Use this Skill for computational musicology with music21: score analysis, harmonic reduction, melodic contour, counterpoint checking, and corpus comparison.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Use this Skill for computational musicology with music21: score analysis, harmonic reduction, melodic contour, counterpoint checking, and corpus comparison.
Query the music21 built-in corpus (Bach chorales, classical works) for statistical analysis
Compare melodic similarity across pieces using edit distance on interval sequences
Build n-gram models of harmonic progressions for style analysis
Task
music21 Entry Point
Load a score file
converter.parse(filepath)
Roman numeral analysis
harmony.romanNumeralFromChord()
Key detection
score.analyze('key') or key.analyze('Krumhansl')
Corpus queries
corpus.search(composer='bach')
Pitch class sets
chord.Chord.pitchClasses
Background
music21 is an MIT-developed Python toolkit for computational musicology. Its object
model mirrors Western music notation: a Score contains one or more Part objects;
each Part is a sequence of Measure objects; each Measure contains Note,
Chord, Rest, and other GeneralNote subclasses.
Core Object Hierarchy
Score
└─ Part (e.g., Soprano, Alto, Tenor, Bass)
└─ Measure (bar number, time signature, key signature)
└─ Note / Chord / Rest
├─ Note: pitch (Pitch object), duration (Duration), offset
└─ Chord: list of pitches, root(), inversion(), pitchClasses
Harmonic Analysis Concepts
Roman numeral analysis: Each chord is labelled relative to the local key
(e.g., I, IV, V7, ii6). harmony.romanNumeralFromChord(chord, key) returns a
RomanNumeral object with .figure (string label) and .scaleDegree.
Pitch class sets: Ignore octave and register; a C major triad = {0, 4, 7}.
Useful for atonal / post-tonal analysis.
Krumhansl-Schmuckler key-finding: Correlates the distribution of pitch-class
durations against major/minor key profiles to estimate the most likely key.
Melodic Contour
Contour reduction (Morris 1987) simplifies a melody by retaining only local maxima
and minima. This enables comparison of melodic shape across transpositions and
rhythmic variations. Edit distance on the sequence of melodic intervals (in semitones)
gives a measure of melodic similarity.
Counterpoint Rules (Two-Voice)
The standard prohibition rules in strict two-voice counterpoint:
Rule
Definition
Parallel fifths
Two voices move in same direction, both intervals are P5
import music21
from music21 import corpus, converter, harmony, key
print(f"music21 version: {music21.__version__}")
# Quick smoke test: load a Bach chorale from the built-in corpus
bwv66 = corpus.parse('bach/bwv66.6')
print(f"Parts: {[p.partName for p in bwv66.parts]}")
print(f"Measures: {len(list(bwv66.parts[0].getElementsByClass('Measure')))}")
Core Workflow
Step 1 — Load a Score and Run Roman Numeral Analysis
import pandas as pd
from music21 import corpus, harmony, roman, chord, key as m21key
defanalyze_bach_chorale_harmony(bwv_id: str = 'bach/bwv66.6') -> pd.DataFrame:
"""
Load a Bach chorale from the built-in corpus and perform Roman numeral
harmonic analysis on every beat-level chord slice.
Args:
bwv_id: corpus path string, e.g. 'bach/bwv66.6'
Returns:
DataFrame with columns: measure, beat, chord_pitches, key, roman_numeral,
scale_degree, chord_quality
"""
score = corpus.parse(bwv_id)
# Detect global key using Krumhansl-Schmuckler algorithm
detected_key = score.analyze('key')
print(f"Detected key: {detected_key} (confidence: {detected_key.correlationCoefficient:.3f})")
# Reduce score to chordified version (one chord per beat position)
chordified = score.chordify()
records = []
for measure in chordified.recurse().getElementsByClass('Measure'):
m_num = measure.number
for element in measure.notes:
ifnotisinstance(element, chord.Chord):
continue# Local key at this position
local_key = element.getContextByClass('KeySignature')
analysis_key = detected_key # fall back to global keytry:
rn = roman.romanNumeralFromChord(element, analysis_key)
figure = rn.figure
scale_deg = rn.scaleDegree
quality = element.commonName
except Exception:
figure = '?'
scale_deg = -1
quality = 'unknown'
records.append({
'measure': m_num,
'beat': float(element.beat),
'chord_pitches': ' '.join(str(p) for p in element.pitches),
'key': str(detected_key),
'roman_numeral': figure,
'scale_degree': scale_deg,
'chord_quality': quality,
})
df = pd.DataFrame(records)
return df
defharmonic_progression_frequency(df: pd.DataFrame, n: int = 2) -> pd.Series:
"""
Compute bigram (or n-gram) frequency of Roman numeral progressions.
Args:
df: Output of analyze_bach_chorale_harmony()
n: n-gram size (default 2 = bigrams like I->V)
Returns:
Series of progression counts, sorted descending.
"""
rn_seq = df['roman_numeral'].tolist()
ngrams = [
' -> '.join(rn_seq[i:i + n])
for i inrange(len(rn_seq) - n + 1)
]
freq = pd.Series(ngrams).value_counts()
return freq
# --- Run ---
df_harmony = analyze_bach_chorale_harmony('bach/bwv66.6')
print(df_harmony.head(10).to_string(index=False))
bigrams = harmonic_progression_frequency(df_harmony, n=2)
print("\nTop 10 harmonic bigrams:")
print(bigrams.head(10))
Step 2 — Melodic Contour and Interval Histogram
import numpy as np
import matplotlib.pyplot as plt
from music21 import corpus, note, interval
defextract_melodic_features(bwv_id: str = 'bach/bwv66.6',
part_index: int = 0) -> dict:
"""
Extract melodic interval sequence, contour reduction, and interval histogram
for a single part of a score.
Args:
bwv_id: corpus path string
part_index: which part to analyse (0=Soprano in Bach chorales)
Returns:
dict with keys: intervals_semitones, contour, interval_counts,
mean_interval, std_interval
"""
score = corpus.parse(bwv_id)
part = score.parts[part_index]
# Collect all Note objects (exclude rests and non-pitched)
notes = [n for n in part.flat.notes ifisinstance(n, note.Note)]
# Compute melodic intervals in semitones
semitones = []
for i inrange(len(notes) - 1):
intv = interval.Interval(notes[i], notes[i + 1])
semitones.append(intv.semitones)
# Contour reduction: keep local extrema (local max and min)defcontour_reduction(seq):
iflen(seq) < 3:
return seq
reduced = [seq[0]]
for i inrange(1, len(seq) - 1):
is_local_max = seq[i] > seq[i - 1] and seq[i] > seq[i + 1]
is_local_min = seq[i] < seq[i - 1] and seq[i] < seq[i + 1]
if is_local_max or is_local_min:
reduced.append(seq[i])
reduced.append(seq[-1])
return reduced
midi_pitches = [n.pitch.midi for n in notes]
contour = contour_reduction(midi_pitches)
# Interval histogram
interval_counts = {}
for s in semitones:
interval_counts[s] = interval_counts.get(s, 0) + 1# Plot
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# Pitch contour
axes[0].plot(midi_pitches, linewidth=0.7, color='steelblue', label='Original')
reduced_x = np.linspace(0, len(midi_pitches) - 1, len(contour))
axes[0].plot(reduced_x, contour, 'ro-', linewidth=1.5, markersize=4, label='Contour reduction')
axes[0].set_title(f'Melodic Contour — {bwv_id} Part {part_index}')
axes[0].set_xlabel('Note index')
axes[0].set_ylabel('MIDI pitch')
axes[0].legend()
# Interval histogram
bins = sorted(interval_counts.keys())
counts = [interval_counts[b] for b in bins]
axes[1].bar(bins, counts, color='coral', edgecolor='black', linewidth=0.5)
axes[1].set_title('Melodic Interval Histogram (semitones)')
axes[1].set_xlabel('Interval (semitones)')
axes[1].set_ylabel('Frequency')
fig.tight_layout()
fig.savefig('melodic_contour.png', dpi=150)
plt.close(fig)
return {
'intervals_semitones': semitones,
'contour': contour,
'interval_counts': interval_counts,
'mean_interval': float(np.mean(semitones)) if semitones else0.0,
'std_interval': float(np.std(semitones)) if semitones else0.0,
}
# --- Run ---
features = extract_melodic_features('bach/bwv66.6', part_index=0)
print(f"Mean interval: {features['mean_interval']:.2f} semitones")
print(f"Std interval: {features['std_interval']:.2f} semitones")
print(f"Contour length after reduction: {len(features['contour'])} points")
Step 3 — Parallel Fifths Detector in Two-Voice Counterpoint
from music21 import corpus, chord, interval, note
from typing importList, Tupledefdetect_parallel_intervals(
score_path: str = 'bach/bwv66.6',
upper_part_idx: int = 0,
lower_part_idx: int = 1,
target_semitones: List[int] = None,
) -> List[dict]:
"""
Detect parallel fifths and parallel octaves between two voices.
A parallel fifth/octave occurs when two consecutive harmonic intervals
of the same size are approached by both voices moving in the same direction.
Args:
score_path: corpus path or file path
upper_part_idx: index of the upper voice part
lower_part_idx: index of the lower voice part
target_semitones: list of interval sizes to flag (default: [7, 12] = P5, P8)
Returns:
List of dicts describing each violation: measure, beat, upper_notes,
lower_notes, interval_size, violation_type
"""if target_semitones isNone:
target_semitones = [7, 12] # Perfect fifth, perfect octave/unison# Load from corpus or filetry:
score = corpus.parse(score_path)
except Exception:
from music21 import converter
score = converter.parse(score_path)
upper = score.parts[upper_part_idx].flat.notes
lower = score.parts[lower_part_idx].flat.notes
# Align notes by offset
upper_notes = [(n.offset, n) for n in upper ifisinstance(n, note.Note)]
lower_notes = [(n.offset, n) for n in lower ifisinstance(n, note.Note)]
# Build dict: offset -> pitch
upper_dict = {off: n for off, n in upper_notes}
lower_dict = {off: n for off, n in lower_notes}
shared_offsets = sorted(set(upper_dict.keys()) & set(lower_dict.keys()))
violations = []
for i inrange(len(shared_offsets) - 1):
off1 = shared_offsets[i]
off2 = shared_offsets[i + 1]
u1, u2 = upper_dict[off1], upper_dict[off2]
l1, l2 = lower_dict[off1], lower_dict[off2]
# Harmonic intervals at each timepoint
harm_int1 = abs(u1.pitch.midi - l1.pitch.midi) % 12
harm_int2 = abs(u2.pitch.midi - l2.pitch.midi) % 12# Motion directions
upper_motion = u2.pitch.midi - u1.pitch.midi
lower_motion = l2.pitch.midi - l1.pitch.midi
same_direction = (upper_motion > 0and lower_motion > 0) or \
(upper_motion < 0and lower_motion < 0)
for target in target_semitones:
# Check modulo 12 for compound intervalsif harm_int1 == target % 12and harm_int2 == target % 12and same_direction:
label = 'parallel fifths'if target == 7else'parallel octaves'# Get measure number
m_num = u2.measureNumber ifhasattr(u2, 'measureNumber') else'?'
violations.append({
'measure': m_num,
'beat_offset': off2,
'upper_notes': f"{u1.nameWithOctave}->{u2.nameWithOctave}",
'lower_notes': f"{l1.nameWithOctave}->{l2.nameWithOctave}",
'interval_semitones': target,
'violation_type': label,
})
return violations
# --- Run ---
violations = detect_parallel_intervals('bach/bwv66.6', upper_part_idx=0, lower_part_idx=3)
if violations:
print(f"Found {len(violations)} parallel interval violations:")
for v in violations[:10]:
print(f" Measure {v['measure']}: {v['violation_type']} — "f"upper {v['upper_notes']} / lower {v['lower_notes']}")
else:
print("No parallel fifths or octaves detected between selected voices.")
Advanced Usage
Corpus Search and Comparative Analysis
from music21 import corpus
import pandas as pd
defcompare_chorale_keys() -> pd.DataFrame:
"""
Search all Bach chorales in the corpus and compare key distribution.
Returns a DataFrame of detected keys and their frequencies.
"""
results = []
# Get all Bach chorale paths
chorale_paths = corpus.getComposer('bach')
for path in chorale_paths[:50]: # limit for demotry:
score = corpus.parse(path)
detected_key = score.analyze('key')
results.append({
'path': str(path),
'key': str(detected_key),
'mode': detected_key.mode,
'tonic': detected_key.tonic.name,
'confidence': round(detected_key.correlationCoefficient, 3),
})
except Exception as e:
continue
df = pd.DataFrame(results)
key_freq = df['key'].value_counts().head(20)
print("Most common keys in Bach chorales:")
print(key_freq)
return df
Melodic Similarity with Edit Distance
defmelodic_edit_distance(seq1: list, seq2: list) -> int:
"""
Compute Levenshtein edit distance between two melodic interval sequences.
Lower distance = more similar melodies.
Args:
seq1: list of semitone intervals for melody 1
seq2: list of semitone intervals for melody 2
Returns:
Integer edit distance.
"""
m, n = len(seq1), len(seq2)
dp = [[0] * (n + 1) for _ inrange(m + 1)]
for i inrange(m + 1):
dp[i][0] = i
for j inrange(n + 1):
dp[0][j] = j
for i inrange(1, m + 1):
for j inrange(1, n + 1):
cost = 0if seq1[i - 1] == seq2[j - 1] else1
dp[i][j] = min(
dp[i - 1][j] + 1, # deletion
dp[i][j - 1] + 1, # insertion
dp[i - 1][j - 1] + cost # substitution
)
return dp[m][n]
Voice Leading Statistics
defvoice_leading_stats(score_path: str) -> dict:
"""
Summarize voice leading motion statistics for a four-voice score:
fraction of oblique, contrary, similar, and parallel motion.
"""from music21 import corpus, note as m21note, converter
try:
score = corpus.parse(score_path)
except Exception:
score = converter.parse(score_path)
parts = score.parts
iflen(parts) < 2:
return {}
motion_counts = {'contrary': 0, 'parallel': 0, 'similar': 0, 'oblique': 0}
for part_a_idx inrange(len(parts) - 1):
for part_b_idx inrange(part_a_idx + 1, len(parts)):
notes_a = [n for n in parts[part_a_idx].flat.notes ifisinstance(n, m21note.Note)]
notes_b = [n for n in parts[part_b_idx].flat.notes ifisinstance(n, m21note.Note)]
min_len = min(len(notes_a), len(notes_b)) - 1for i inrange(min_len):
ma = notes_a[i + 1].pitch.midi - notes_a[i].pitch.midi
mb = notes_b[i + 1].pitch.midi - notes_b[i].pitch.midi
if ma == 0or mb == 0:
motion_counts['oblique'] += 1elif ma > 0and mb < 0or ma < 0and mb > 0:
motion_counts['contrary'] += 1elif ma == mb:
motion_counts['parallel'] += 1else:
motion_counts['similar'] += 1
total = sum(motion_counts.values())
return {k: round(v / total, 3) if total else0.0for k, v in motion_counts.items()}
Troubleshooting
Error
Cause
Fix
SubConverterException: Cannot find a path to MuseScore
MuseScore not installed or not on PATH
Install MuseScore or use score.show('text') for text output
CorpusException: Could not find path 'bach/...'
Incorrect corpus path
Use corpus.search('bach') to list available paths
AttributeError: 'Rest' object has no attribute 'pitch'