Skip to content

Latest commit

 

History

History
496 lines (425 loc) · 26.1 KB

README.md

File metadata and controls

496 lines (425 loc) · 26.1 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
from_noteworthy(text) NoteWorthy Composer text, .nwctxt Score
from_musedata(texts) MuseData, one file's text or a list of a work's Score
from_capella(data) a Capella .capx's bytes, or its score.xml Score
from_nwc(data) a NoteWorthy Composer .nwc file's bytes, compressed or not Score
from_volpiano(text) Volpiano, the chant font's notation Part

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 are RomanNumerals, carrying their figures as lyrics. a score's metadata comes with it, as a Metadata, so a collection read from one file can be asked for a tune by its number or its title. what the package has no class for -- repeat marks, unpitched percussion, a fermata -- is read and left out. a note's articulations and its ornaments come with it, and so do barlines, repeats, words and the spanners: a measure carries its leftBarline and rightBarline, its number, numberSuffix and padding, and answers barDuration, barDurationProportion, bestTimeSignature and padAsAnacrusis; a part carries its partName and partAbbreviation, answering for the first instrument it holds where nothing says them; a stream answers finalBarline, spanners and spannerBundle and plays its repeats out with expandRepeats; and a slur names the score's own notes. input a reader cannot read raises StreamException. a compressed .mxl is a zip holding the document: unpack it first.

the package's streams slice, scale, invert and regroup themselves as music21's do -- sliceByQuarterLengths, sliceAtOffsets, sliceByBeat, sliceByGreatestDivisor, scaleOffsets, scaleDurations, augmentOrDiminish, invertDiatonic, voicesToParts, explode, partsToVoices, implode, transpose, stripTies, quantize and template -- each handing back a new stream, so inPlace=True and a target are refused. they notate themselves too -- makeMeasures, makeRests, makeTies, makeBeams, makeAccidentals, makeVoices and makeNotation, each step at music21's own defaults, a keyword asking for anything else refused rather than answered differently. splitAtQuarterLength cuts a stream in two at a length from its start, and extendTies and shiftElements change the stream and answer nothing, as music21's do -- the ties written onto its own notes. and they answer highestOffset, lowestOffset, hasMeasures, hasVoices, isGapless, isSequence and isWellFormedNotation, hasPartLikeStreams, findGaps, getOverlaps and simultaneousAttacks, and they take elements out by class or by index (removeByClass, removeByNotOfClass, pop), repeat an object into themselves as copies (repeatAppend, repeatInsert), make room for one (insertAndShift), merge a note into what already stands at an offset (insertIntoNoteOrChord), divide themselves by what a function says of each element (splitByClass) and answer voices and what they open with -- clef, keySignature and timeSignature, read and written as music21's are -- and the meters and instruments they hold, getTimeSignatures, getInstruments and getInstrument, which make up music21's default where a stream states none -- measures and measure -- the measures of a number range, the first at nought and before it what was in force where it begins -- and measureOffsetMap, which a score builds out of its parts, and offsetMap, music21's named tuple per element, pitches, extractContext, playingWhenAttacked and allPlayingWhileSounding -- besides chordify, findConsecutiveNotes and melodicIntervals. a part written for a transposing instrument is turned to sounding pitch and back with toSoundingPitch and toWrittenPitch, and says which it is with atSoundingPitch -- True, False or 'unknown', as music21's does. they are asked for elements by class and by offset -- a class being read as music21 reads it, by its own name or by anything it is a kind of, so getElementsByClass('GeneralNote') finds the notes, the chords and the rests -- which is what every object's classes says of itself, music21's own chain down to object -- getElementsByClass, getElementsNotOfClass, hasElementOfClass and getElementsByOffset with music21's four keywords, under which an element is found only where it begins; a class may be given as the class itself as well as by name, which is how music21's own code asks. recurse answers what music21's answers, a StreamIterator holding what the walk found in the walk's own order, which can be filtered again -- recurse().notes -- counted, indexed, iterated and gathered back into a stream with stream(); a stream answers first and last too. Besides those: getElementAtOrBefore, getElementBeforeOffset, getElementAfterElement and beatAndMeasureFromOffset -- and they answer isTwelveTone and staffLines. they stretch, take in and empty themselves as well: extendDuration gives each element of a class the time up to the next one, mergeElements takes in everything another stream holds, each at the offset it stands at there and the object itself, and cloneEmpty is a stream of the same class holding nothing. seconds says how long a stream lasts and metronomeMarkBoundaries which tempo holds through each stretch of it, handing back the stream's own mark wherever it holds one; where a stream states no tempo a quarter at 120 is counted, as upstream, and seconds is nan, which is music21's answer for a stream whose own search finds no tempo. lyrics gathers every line sung, by the number it is sung under, with a place for each note read so the lines stay level with the notes and the note's own Lyric in it.

a chord diagram is a FretBoard of FretNotes -- GuitarFretBoard, UkeleleFretBoard, BassGuitarFretBoard and MandolinFretBoard are tuned as those instruments are -- and getPitches says what it sounds, each string's open pitch raised by the fret stopping it.

a **harm token is read as the roman numeral it annotates with convertHarmToRoman and taken apart by a HarmParser, and an ActivityMatch says what pitch sounds under which dynamic.

a stream's notes are hashed into comparable tuples by a Hasher, by pitch, length and offset or whatever of the sixteen settings is asked for, each hash saying what it was made from where asked.

the elements braille writes together are a BrailleElementGrouping, which mimics a list as music21's does, and a NoteGroupingTranscriber writes one, element by element or all at once, saying of each what its signs mean.

a score, a part, a measure or an opus is written in braille with objectToBraille and the seven names beside it -- streamToBraille, scoreToBraille, partToBraille, measureToBraille, opusToBraille, keyboardPartsToBraille for a piano's two staves, and metadataToString for what stands above the music -- each taking music21's own keywords.

music is written in braille one element at a time -- noteToBraille, chordToBraille, restToBraille, keySigToBraille, timeSigToBraille, clefToBraille, dynamicToBraille, metronomeMarkToBraille, instrumentToBraille, textExpressionToBraille and transcribeHeading among them, each writing what its signs say onto the element it read -- and laid out in lines of so many cells by a BrailleText, a BrailleKeyboard for the two hands, or a BrailleTextLine on its own.

a stream is searched for a row of pitch classes with a ContiguousSegmentSearcher, which finds every run of a length, and with a SegmentMatcher or one of the five matchers beside it, which find the runs that are the segments searched for -- transposed, transformed, or as sets in any order; labelSegments and its five kin mark a copy of the stream with a line over each run found and its name sung under its first note.

a stream is searched for a run of notes with a StreamSearcher, or with rhythmicSearch, noteNameSearch and noteNameRhythmicSearch, which say where each run starts; a line is written as text with translateStreamToString and its four cousins, so that two lines can be compared with approximateNoteSearch and the three beside it; and mostCommonMeasureRhythms says which rhythms a stream's measures share.

a part is written as text and cut into overlapping stretches with translateMonophonicPartToSegments, a whole score part by part with indexScoreParts, and scoreSimilarity says how alike every stretch of some indexed scores is, by difflib's ratio.

the words sung in a score are searched with a LyricSearcher: indexText is each verse read as one text, index every lyric with where its text sits there, and search every place a string or a compiled regular expression is sung, each match naming the score's own notes and their own lyrics.

several drums struck at once are a PercussionChord, which music21 says is not a chord -- there is no pitch to compare -- holding whatever it was handed, the members that sound answering pitches, and writing those pitches replacing every member with a note of them. PercussionMapper reads the General MIDI percussion map either way: midiPitchToInstrument takes a key as a number or as a pitch and says which drum it plays and how it is played, key 36 being a bass drum played 1 and key 35 the same drum played acoustic, and midiInstrumentToPitch says the key a drum is struck on.

a score's words are read off its notes with assembleLyrics and assembleAllLyrics, a title's article is moved to the front of it and back with prependArticle and postpendArticle, and a TextBox stands words anywhere on a page, drawn with music21's own TextStyle.

how a score, a page, a system and a staff are laid out is here as ScoreLayout, PageLayout, SystemLayout and StaffLayout, each measurement written back the way music21 writes it and a score layout holding the page, the system and the staves it starts with as the objects music21 holds them.

music21's repeat marks are here -- Coda, Segno, Fine, DaCapo, DaCapoAlFine, DaCapoAlCoda, AlSegno, DalSegno, DalSegnoAlFine and DalSegnoAlCoda -- each saying what it may be written with (isValidText), what it is written with (getText, setText) and whether it is drawn as its sign (useSymbol); music21's own Expander plays a score's repeats out over them, and the crate does it in one function.

a pitch, a note or a chord is read from what it is called in German, French, Italian or Spanish with toPitch, toNote and toChord, and generateLanguageDictionary writes out every name a language has; a name the language calls no pitch by is a C natural, as music21 answers.

music21's analysis.patel and analysis.metrical are here -- nPVI, melodicIntervalVariability, labelBeatDepth and thomassenMelodicAccent -- and a stream writes the interval against another part onto its notes with attachIntervalsBetweenStreams. thomassenMelodicAccent and attachIntervalsBetweenStreams leave their answer on the note's editorial, music21's dictionary-with-attributes for what an analysis found, and labelBeatDepth writes a lyric of stars under each note; all three write onto the stream they are given, as music21's do. music21's own doctests for both modules run against these.

analysis.segmentByRests.Segmenter cuts a line into the runs of notes between its rests, each run the stream's own notes and each interval built on two of them; analysis.pitchAnalysis.pitchAttributeCount counts a named property of every pitch into a Counter, and analysis.elements.attributeCount counts a named property of every element a stream or an iterator hands over, passing over an element that has none. analysis.floatingKey.KeyAnalyzer reads a key for every measure and smooths each by the measures around it, through a window and a weighting the caller may say. the figured-bass realizer is here as FiguredBassLine and Realization: a line of bass notes and figures in a key and a meter, written out as a part, and every voicing of it that keeps the rules -- counted, listed, chosen from and written out as a score in keyboard or chorale style, with any part laid over the line kept as it stands. where music21 chooses at random this chooses through Python's own random, so a caller who seeds it gets the same realization twice.

analysis.windowed.WindowedAnalysis runs a processor's own analysis over windows of a stream, a quarter note long and up, each window handed over as a stream of the package's; the answer and the colour are the processor's.

analysis.discrete's classes are here too -- Ambitus, the five key weightings and MelodicIntervalDiversity -- each handing back what the stream holds: a span is its own pitch objects, and a counted interval is built on the two notes it was measured between. process is left out for Ambitus and for a key weighting, answering a colour out of music21's colour tables, which the crate does not carry.

music21's four instrument stream functions are here too -- partitionByInstrument, bundleInstruments, unbundleInstruments and deduplicate -- each handing back a new stream.

a score carries its metadata, music21's Metadata, whose names are read and written as music21 reads them -- by a unique name, by one of the four grandfathered work ids, or by one of the twenty Humdrum abbreviations, so md.otl and md.title are the same title -- and which answers search, all and md['title']. what a name answers is a string, where music21 answers its own Text and Contributor, so all(returnPrimitives=True) is refused and nothing here is installed over music21's own metadata. an Opus -- what a multi-tune ABC or Essen file reads as -- answers scores, getNumbers, getScoreByNumber, getScoreByTitle and mergeScores.

analyze asks a stream one question, as music21's does: ambitus, span and range give the interval from the lowest pitch sounding to the highest, and key and the names of music21's five key weightings give the key the stream is most likely in. the name need not be exact -- kostka finds Temperley's profile, a bare key finds Aarden's -- and a stream with no pitches has no ambitus, answering None as music21 does.

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.

to_midi takes the same and hands back the bytes of the MIDI file music21's streamToMidiFile writes, its repeats played out first, and to_abc writes it as ABC, which music21 has no writer for: a tune music21's reader and this package's both read back as the same score. to_volpiano writes it as Volpiano, as music21's volpiano.fromStream does, and to_roman_text writes its roman numerals as a RomanText analysis, as music21's RnWriter does. to_lilypond writes it as LilyPond, as music21's LilypondConverter does, with no LilyPond installed: version is the LilyPond version the file says it is for, 2.24 unless given.

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,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 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: