|
#!/usr/bin/env python3 |
|
""" |
|
Demonstration of rhythmic resolution in 4/4: |
|
- Dotted eighths (3/16) take 3 measures to realign with the bar |
|
- Eighth-note triplets resolve cleanly every measure |
|
|
|
We place the two patterns in parallel parts (both 4/4) to show: |
|
- The triplet line lands back on beat 1 every bar |
|
- The dotted-eighth line only lands back on beat 1 after 3 bars (LCM of 16 and 3 = 48 sixteenths = 3 measures) |
|
""" |
|
|
|
from music21 import ( |
|
stream, |
|
meter, |
|
note, |
|
tempo, |
|
metadata, |
|
layout, |
|
clef, |
|
expressions, |
|
duration, |
|
environment, |
|
tie, |
|
) |
|
|
|
|
|
def create_dotted_eighth_example_4_4(): |
|
""" |
|
Dotted-eighth (3/16) pattern in 4/4. Realigns every 3 measures. |
|
No rests; ties are used when a dotted eighth crosses a barline. |
|
- Measure length: 16 sixteenths |
|
- Step size: 3 sixteenths |
|
- LCM(16, 3) = 48 sixteenths = 3 measures to land back on beat 1 |
|
""" |
|
part = stream.Part() |
|
part.insert(0, clef.TrebleClef()) |
|
part.insert(0, meter.TimeSignature("4/4")) |
|
part.insert(0, tempo.MetronomeMark(number=96)) |
|
part.partName = "Dotted 1/8 pattern (realigns in 3 bars)" |
|
|
|
pitches = ["C4", "D4"] # simple two-note cell |
|
dotted_q_len = 0.75 # dotted eighth in quarter-length units |
|
|
|
measures = [stream.Measure(number=i + 1) for i in range(3)] |
|
measure_idx = 0 |
|
remaining_in_measure = 4.0 |
|
pending_tie_stop = False |
|
|
|
for i in range(16): # 16 dotted eighths = 3 measures exactly |
|
pitch = pitches[i % len(pitches)] |
|
dur_left = dotted_q_len |
|
|
|
while dur_left > 0 and measure_idx < len(measures): |
|
available = remaining_in_measure |
|
this_len = min(dur_left, available) |
|
|
|
n = note.Note(pitch) |
|
n.duration.quarterLength = this_len |
|
|
|
# Manage ties across barlines |
|
if pending_tie_stop and dur_left == dotted_q_len: |
|
n.tie = tie.Tie("stop") |
|
pending_tie_stop = False |
|
if dur_left > available: |
|
# Will continue into next bar |
|
n.tie = tie.Tie("start") |
|
pending_tie_stop = True |
|
|
|
measures[measure_idx].append(n) |
|
|
|
dur_left -= this_len |
|
remaining_in_measure -= this_len |
|
|
|
if remaining_in_measure <= 1e-9: |
|
measure_idx += 1 |
|
if measure_idx < len(measures): |
|
remaining_in_measure = 4.0 |
|
|
|
# Annotations |
|
if measures: |
|
start_text = expressions.TextExpression("Starts on beat 1") |
|
start_text.style.absoluteY = 20 |
|
measures[0].insert(0, start_text) |
|
if len(measures) >= 3: |
|
resolve_text = expressions.TextExpression("Realigns on beat 1 (after 3 bars)") |
|
resolve_text.style.absoluteY = 20 |
|
measures[2].insert(0, resolve_text) |
|
|
|
for m in measures: |
|
part.append(m) |
|
|
|
return part |
|
|
|
|
|
def create_triplet_example_4_4(measures: int = 1, name: str = "1/8 triplets"): |
|
""" |
|
Eighth-note triplets in 4/4. Realigns every bar. |
|
measures: number of bars to illustrate repeated resolution. |
|
""" |
|
part = stream.Part() |
|
part.insert(0, clef.TrebleClef()) |
|
part.insert(0, meter.TimeSignature("4/4")) |
|
part.insert(0, tempo.MetronomeMark(number=96)) |
|
part.partName = name |
|
part.partAbbreviation = name |
|
|
|
pitches = ["C4", "D4", "E4"] |
|
|
|
for m_idx in range(measures): |
|
measure = stream.Measure(number=m_idx + 1) |
|
# 4 groups of triplets fill a 4/4 measure |
|
for _ in range(4): |
|
for pitch in pitches: |
|
n = note.Note(pitch) |
|
n.duration.type = "eighth" |
|
n.duration.tuplets = [duration.Tuplet(3, 2)] |
|
measure.append(n) |
|
|
|
if m_idx == 0: |
|
text_exp = expressions.TextExpression("Realigns every bar") |
|
text_exp.style.absoluteY = 20 |
|
measure.insert(0, text_exp) |
|
part.append(measure) |
|
return part |
|
|
|
|
|
def create_score_4_4(): |
|
""" |
|
Single score comparing two 4/4 parts: |
|
- Upper staff: dotted eighth pattern (realigns every 3 bars) |
|
- Lower staff: triplet pattern over 1 bar (realigns every bar) |
|
""" |
|
score = stream.Score() |
|
score.metadata = metadata.Metadata() |
|
score.metadata.title = "4/4 Rhythmic Resolution" |
|
score.metadata.composer = "Educational Example" |
|
score.metadata.subtitle = "Dotted 1/8 (3-bar cycle) vs 1/8 triplets (1-bar cycle)" |
|
|
|
dotted_part = create_dotted_eighth_example_4_4() |
|
triplet_part_single = create_triplet_example_4_4(measures=1, name="Triplets (1 bar)") |
|
|
|
score.insert( |
|
0, |
|
layout.StaffGroup( |
|
[dotted_part, triplet_part_single], |
|
name="Dotted vs Triplets", |
|
abbreviation="Dots vs Trips", |
|
), |
|
) |
|
|
|
score.append(dotted_part) |
|
score.append(triplet_part_single) |
|
return score |
|
|
|
|
|
def main(): |
|
""" |
|
Generate and display the musical examples. |
|
""" |
|
# Point music21 to Guitar Pro 8 for opening MusicXML if available |
|
env = environment.UserSettings() |
|
gp_path = "/Applications/Guitar Pro 8.app/Contents/MacOS/GuitarPro" |
|
try: |
|
env["musicxmlPath"] = gp_path |
|
except Exception as exc: |
|
# If setting fails, continue; user can open the generated MXL files manually |
|
print(f"Warning: could not set musicxmlPath to Guitar Pro 8 ({exc})") |
|
|
|
print("=" * 70) |
|
print("4/4 Rhythmic Resolution Demo") |
|
print("=" * 70) |
|
print() |
|
print("This shows two simultaneous 4/4 patterns:") |
|
print(" - Dotted 1/8 pattern (3/16 step) realigns every 3 bars") |
|
print(" - 1/8 triplets realign every bar") |
|
print() |
|
|
|
score = create_score_4_4() |
|
|
|
# Show the score |
|
print("Displaying score in music21 (uses Guitar Pro if available)...") |
|
score.show("musicxml") |
|
|
|
# Save single output |
|
output_dir = __import__("pathlib").Path(__file__).parent |
|
score_path = output_dir / "rhythmic_resolution_4-4.mxl" |
|
|
|
print(f"\nSaving score to: {score_path}") |
|
score.write("musicxml", score_path) |
|
|
|
print("\n✓ Done! One file generated. Open in your notation software if viewer not configured.") |
|
|
|
|
|
if __name__ == '__main__': |
|
main() |