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

FormatReadWriteNotes
MusicXMLBest interchange format, lossless
MIDILoses some notation details
Humdrum (**kern)Popular in musicology
ABC notationCommon for folk music
LilypondPublication-quality engraving
MEIMusic 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 pickle or music21.freezeThaw to serialize parsed objects
  • Parallelize: Use multiprocessing to parse and analyze scores concurrently
  • Filter early: Use getElementsByClass and getElementsByOffset to 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:

  1. Parse MIDI/MusicXML files with music21
  2. Extract sequences: pitch, duration, offset, key, chord labels
  3. Encode as integers (pitch → MIDI number, duration → quantized bins)
  4. Feed to a Transformer or LSTM in PyTorch/TensorFlow
  5. Decode model output back to music21 objects
  6. 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.

pythonmusic21music-theorycompositionanalysismusicology

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.