Python Music21 Music Theory — Deep Dive
Stream architecture
Music21’s object hierarchy mirrors how musicians think about scores:
Score
├── Part (Soprano)
│ ├── Measure 1
│ │ ├── TimeSignature (4/4)
│ │ ├── KeySignature (2 sharps)
│ │ ├── Note (D5, quarter)
│ │ └── Note (E5, quarter)
│ └── Measure 2
│ └── ...
├── Part (Alto)
└── Part (Bass)
Every element has an offset — its position in quarter-note units from the beginning of its container. stream.recurse() flattens the hierarchy for iteration. stream.flatten() creates a flat copy with absolute offsets.
Element addressing
from music21 import converter
score = converter.parse("bach/bwv66.6")
soprano = score.parts[0]
# Get all notes and chords (skip rests, time signatures, etc.)
notes = soprano.recurse().notes
# Get elements at a specific offset
elements_at_beat_3 = soprano.flatten().getElementsByOffset(2.0, 3.0)
# Filter by class
key_sigs = score.recurse().getElementsByClass("KeySignature")
Derivation chain
When you filter or transform a stream, music21 tracks the derivation — which operations produced the new stream from the original. This provenance is useful for debugging complex analysis pipelines.
Advanced harmonic analysis
Chord symbol identification
from music21 import chord
c = chord.Chord(["C3", "E3", "G3", "Bb3"])
print(c.commonName) # 'Dominant Seventh Chord'
print(c.quality) # 'dominant'
print(c.inversion()) # 0 (root position)
print(c.closedPosition()) # normalizes voicing
Roman numeral analysis on real scores
from music21 import converter, analysis
score = converter.parse("bach/bwv66.6")
chords = score.chordify() # reduces all parts to a chord stream
key = score.analyze("key")
for c in chords.recurse().getElementsByClass("Chord"):
rn = analysis.roman.romanNumeralFromChord(c, key)
print(f"Beat {c.offset}: {rn.figure}")
chordify() vertically slices simultaneous notes into chord objects. This enables harmonic rhythm analysis — how frequently chords change and which progressions dominate.
Key-finding algorithms
Music21 implements multiple key-finding methods:
- Krumhansl-Schmuckler: Correlates pitch-class distribution with key profiles derived from psychological experiments
- Bellman-Budge: Uses different profiles based on music history
- Temperley: Uses a Bayesian model with prior probabilities for different keys
- Aarden-Essen: Profiles derived from large folk song corpora
key_analysis = score.analyze("key")
print(key_analysis.correlationCoefficient) # confidence
print(key_analysis.alternateInterpretations[:5]) # runner-up keys
Voice leading and counterpoint
Music21 can check species counterpoint rules:
from music21 import voiceLeading
vl = voiceLeading.VoiceLeadingQuartet(
note.Note("C4"), note.Note("D4"), # voice 1
note.Note("E4"), note.Note("F4") # voice 2
)
print(vl.parallelMotion) # True/False
print(vl.similarMotion)
print(vl.contraryMotion)
print(vl.obliqueMotion)
For detecting parallel fifths and octaves (forbidden in classical counterpoint):
from music21 import analysis
# Check all adjacent chord pairs for voice-leading errors
parts = score.parts
for i in range(len(parts) - 1):
for j in range(i + 1, len(parts)):
vlqs = voiceLeading.VoiceLeadingQuartet.fromNotes(
parts[i].flatten().notes, parts[j].flatten().notes
)
# Check for parallel fifths, hidden octaves, etc.
Corpus-scale research
Statistical analysis across Bach chorales
from music21 import corpus, features
from collections import Counter
interval_counts = Counter()
for path in corpus.getComposer("bach")[:50]:
score = corpus.parse(path)
melody = score.parts[0].flatten().notes
for i in range(len(melody) - 1):
intv = interval.Interval(melody[i], melody[i+1])
interval_counts[intv.name] += 1
# Most common melodic intervals
for name, count in interval_counts.most_common(10):
print(f"{name}: {count}")
Feature extraction for ML
Music21’s features module provides pre-built feature extractors compatible with the jSymbolic feature set:
from music21 import features
score = corpus.parse("bach/bwv66.6")
ds = features.DataSet(classLabel="Composer")
ds.addData(score, classValue="Bach")
# Extract all jSymbolic features
fe = features.jSymbolic.getCompletionStats()
For custom feature extraction, iterate over the stream hierarchy:
def extract_custom_features(score):
melody = score.parts[0].flatten().notes
pitches = [n.pitch.midi for n in melody]
durations = [n.quarterLength for n in melody]
return {
"pitch_range": max(pitches) - min(pitches),
"mean_pitch": sum(pitches) / len(pitches),
"pitch_entropy": scipy.stats.entropy(np.bincount(pitches, minlength=128)),
"note_density": len(melody) / score.highestTime,
"mean_duration": sum(durations) / len(durations),
"key": str(score.analyze("key")),
}
Transformations and generation
Transposition and inversion
# Transpose up a major third
transposed = score.transpose("M3")
# Retrograde (reverse)
from music21 import stream
melody = score.parts[0].flatten().notes
retro = stream.Stream()
for n in reversed(list(melody)):
retro.append(n)
# Inversion (mirror around a pivot pitch)
pivot = note.Note("C5").pitch.midi
inverted = stream.Stream()
for n in melody:
new_midi = 2 * pivot - n.pitch.midi
inverted.append(note.Note(new_midi, quarterLength=n.quarterLength))
Algorithmic composition with theory constraints
from music21 import scale, chord, stream, note
import random
sc = scale.MinorScale("A")
scale_pitches = [p.midi for p in sc.getPitches("A3", "A5")]
def compose_constrained_melody(length=16, scale_pitches=scale_pitches):
melody = stream.Stream()
current = random.choice(scale_pitches)
for _ in range(length):
# Constrain: steps or small leaps only
candidates = [p for p in scale_pitches if abs(p - current) <= 4]
current = random.choice(candidates)
melody.append(note.Note(current, quarterLength=random.choice([0.5, 1.0, 1.0, 2.0])))
return melody
Serialization and interchange
| Format | Read | Write | Notes |
|---|---|---|---|
| MusicXML | ✅ | ✅ | Best interchange format, lossless |
| MIDI | ✅ | ✅ | Loses some notation details |
| Humdrum (**kern) | ✅ | ✅ | Popular in musicology |
| ABC notation | ✅ | ✅ | Common for folk music |
| Lilypond | ❌ | ✅ | Publication-quality engraving |
| MEI | ✅ | ✅ | Music Encoding Initiative XML |
Performance considerations
Music21 is rich but not fast. Parsing a large orchestral score can take seconds. For corpus-scale analysis:
- Cache parsed scores: Use
pickleormusic21.freezeThawto serialize parsed objects - Parallelize: Use
multiprocessingto parse and analyze scores concurrently - Filter early: Use
getElementsByClassandgetElementsByOffsetto avoid iterating everything - Avoid
.show()in loops: Rendering notation is expensive; accumulate results and display at the end
For datasets of 10K+ scores, consider extracting features once and storing them as CSV/Parquet rather than re-parsing each time.
Integration with ML frameworks
A typical pipeline for training a music generation model:
- Parse MIDI/MusicXML files with music21
- Extract sequences: pitch, duration, offset, key, chord labels
- Encode as integers (pitch → MIDI number, duration → quantized bins)
- Feed to a Transformer or LSTM in PyTorch/TensorFlow
- Decode model output back to music21 objects
- Export as MIDI or MusicXML
# Encoding example
DURATION_BINS = {0.25: 0, 0.5: 1, 1.0: 2, 1.5: 3, 2.0: 4, 4.0: 5}
def encode_melody(part):
tokens = []
for n in part.recurse().notes:
pitch_tok = n.pitch.midi
dur_tok = DURATION_BINS.get(n.quarterLength, 2) # default to quarter
tokens.append((pitch_tok, dur_tok))
return tokens
One thing to remember: Music21 bridges the gap between music notation and computation — its deep modeling of Western music theory, combined with a large built-in corpus, makes it the foundation for serious computational musicology and theory-aware music generation.
See Also
- Python Arcade Library Think of a magical art table that draws your game characters, listens when you press buttons, and cleans up the mess — that's Python Arcade.
- Python Audio Fingerprinting Ever wonder how Shazam identifies a song from just a few seconds of noisy audio? Audio fingerprinting is the magic behind it, and Python can do it too.
- Python Barcode Generation Picture the stripy labels on grocery items to understand how Python can create those machine-readable barcodes from numbers.
- Python Cellular Automata Imagine a checkerboard where each square follows simple rules to turn on or off — and suddenly complex patterns emerge like magic.
- Python Godot Gdscript Bridge Imagine speaking English to a friend who speaks French, with a translator in the middle — that's how Python talks to the Godot game engine.