diff --git a/examples/song.py b/examples/song.py index 9dc9802..1089803 100644 --- a/examples/song.py +++ b/examples/song.py @@ -1,78 +1,207 @@ -from time import sleep +"""Play melodies and chord progressions with PyTheory. -from pytheory import TonedScale, Tone, CHARTS, play +Requires PortAudio: brew install portaudio (macOS) +""" + +from pytheory import Tone, Chord, Key, TonedScale, play, Synth + +# ── Helpers ───────────────────────────────────────────────────────────── + +BPM = 120 +BEAT = 60_000 // BPM # ms per beat -# Add this constant at the top of the file, after the imports -EIGHTH_NOTE = 0.25 -QUARTER_NOTE = 0.5 - -# Add scale definition after the constants -C_MAJOR = TonedScale(tonic="C4") +def play_melody(notes, synth=Synth.SINE): + """Play a sequence of (note_string, beats) tuples.""" + for note, beats in notes: + if note == "REST": + import time + time.sleep(beats * BEAT / 1000) + else: + tone = Tone.from_string(note, system="western") + play(tone, synth=synth, t=int(beats * BEAT)) -def play_note(note, t=0.1): - # Convert scale degree (1-7) to note name (0-based index) - scale_notes = ["C4", "D4", "E4", "F4", "G4", "A4", "B4"] - note_name = scale_notes[note - 1] # Subtract 1 because scale degrees are 1-based - tone = Tone(note_name) - play(tone, t=t * 1_000) - sleep(t) +def play_progression(chords, beats_each=2, synth=Synth.TRIANGLE): + """Play a list of Chord objects.""" + for chord in chords: + name = chord.identify() or "?" + tones = " ".join(t.full_name for t in chord.tones) + print(f" {name:20s} {tones}") + play(chord, synth=synth, t=int(beats_each * BEAT)) -# Twinkle Twinkle Little Star in C major -# C C G G A A G (first line) -# F F E E D D C (second line) -# G G F F E E D (third line) -# G G F F E E D (fourth line) -# C C G G A A G (fifth line) -# F F E E D D C (sixth line) +# ── Songs ─────────────────────────────────────────────────────────────── +def twinkle_twinkle(): + """Twinkle Twinkle Little Star — C major.""" + print("Twinkle Twinkle Little Star") + print("=" * 40) -def play_twinkle(): - # Define the patterns using scale degrees instead of note names - line1 = [ - (1, EIGHTH_NOTE), # C4 - (1, EIGHTH_NOTE), # C4 - (5, EIGHTH_NOTE), # G4 - (5, EIGHTH_NOTE), # G4 - (6, EIGHTH_NOTE), # A4 - (6, EIGHTH_NOTE), # A4 - (5, QUARTER_NOTE), # G4 - ] - line2 = [ - (4, EIGHTH_NOTE), # F4 - (4, EIGHTH_NOTE), # F4 - (3, EIGHTH_NOTE), # E4 - (3, EIGHTH_NOTE), # E4 - (2, EIGHTH_NOTE), # D4 - (2, EIGHTH_NOTE), # D4 - (1, QUARTER_NOTE), # C4 - ] - line3 = [ - (5, EIGHTH_NOTE), # G4 - (5, EIGHTH_NOTE), # G4 - (4, EIGHTH_NOTE), # F4 - (4, EIGHTH_NOTE), # F4 - (3, EIGHTH_NOTE), # E4 - (3, EIGHTH_NOTE), # E4 - (2, QUARTER_NOTE), # D4 + melody = [ + # Twinkle twinkle little star + ("C4", 1), ("C4", 1), ("G4", 1), ("G4", 1), + ("A4", 1), ("A4", 1), ("G4", 2), + # How I wonder what you are + ("F4", 1), ("F4", 1), ("E4", 1), ("E4", 1), + ("D4", 1), ("D4", 1), ("C4", 2), + # Up above the world so high + ("G4", 1), ("G4", 1), ("F4", 1), ("F4", 1), + ("E4", 1), ("E4", 1), ("D4", 2), + # Like a diamond in the sky + ("G4", 1), ("G4", 1), ("F4", 1), ("F4", 1), + ("E4", 1), ("E4", 1), ("D4", 2), + # Twinkle twinkle little star + ("C4", 1), ("C4", 1), ("G4", 1), ("G4", 1), + ("A4", 1), ("A4", 1), ("G4", 2), + # How I wonder what you are + ("F4", 1), ("F4", 1), ("E4", 1), ("E4", 1), + ("D4", 1), ("D4", 1), ("C4", 2), ] - # Construct the full melody using the patterns - melody = ( - line1 # Twinkle twinkle little star - + line2 # How I wonder what you are - + line3 # Up above the world so high - + line3 # Like a diamond in the sky - + line1 # Twinkle twinkle little star - + line2 # How I wonder what you are - ) + play_melody(melody) - print("Playing Twinkle Twinkle Little Star...") - for note, duration in melody: - play_note(note, duration) +def ode_to_joy(): + """Ode to Joy — Beethoven's 9th Symphony, D major.""" + print("Ode to Joy (Beethoven)") + print("=" * 40) + + melody = [ + # Main theme + ("F#4", 1), ("F#4", 1), ("G4", 1), ("A4", 1), + ("A4", 1), ("G4", 1), ("F#4", 1), ("E4", 1), + ("D4", 1), ("D4", 1), ("E4", 1), ("F#4", 1), + ("F#4", 1.5), ("E4", 0.5), ("E4", 2), + # Repeat with variation + ("F#4", 1), ("F#4", 1), ("G4", 1), ("A4", 1), + ("A4", 1), ("G4", 1), ("F#4", 1), ("E4", 1), + ("D4", 1), ("D4", 1), ("E4", 1), ("F#4", 1), + ("E4", 1.5), ("D4", 0.5), ("D4", 2), + ] + + play_melody(melody) + + +def happy_birthday(): + """Happy Birthday — G major.""" + print("Happy Birthday") + print("=" * 40) + + melody = [ + # Happy birthday to you + ("G4", 0.75), ("G4", 0.25), ("A4", 1), ("G4", 1), + ("C5", 1), ("B4", 2), + # Happy birthday to you + ("G4", 0.75), ("G4", 0.25), ("A4", 1), ("G4", 1), + ("D5", 1), ("C5", 2), + # Happy birthday dear [name] + ("G4", 0.75), ("G4", 0.25), ("G5", 1), ("E5", 1), + ("C5", 1), ("B4", 1), ("A4", 2), + # Happy birthday to you + ("F5", 0.75), ("F5", 0.25), ("E5", 1), ("C5", 1), + ("D5", 1), ("C5", 2), + ] + + play_melody(melody) + + +def fur_elise(): + """Fur Elise — opening bars (A minor).""" + print("Fur Elise (opening)") + print("=" * 40) + + melody = [ + ("E5", 0.5), ("D#5", 0.5), ("E5", 0.5), ("D#5", 0.5), + ("E5", 0.5), ("B4", 0.5), ("D5", 0.5), ("C5", 0.5), + ("A4", 1), ("REST", 0.5), + ("C4", 0.5), ("E4", 0.5), ("A4", 0.5), + ("B4", 1), ("REST", 0.5), + ("E4", 0.5), ("G#4", 0.5), ("B4", 0.5), + ("C5", 1), ("REST", 0.5), + ("E4", 0.5), ("E5", 0.5), ("D#5", 0.5), + ("E5", 0.5), ("D#5", 0.5), ("E5", 0.5), ("B4", 0.5), + ("D5", 0.5), ("C5", 0.5), + ("A4", 1), + ] + + play_melody(melody) + + +def pop_progression(): + """The I–V–vi–IV pop progression in C major.""" + print("Pop Progression (I-V-vi-IV in C)") + print("=" * 40) + print() + + key = Key("C", "major") + chords = key.progression("I", "V", "vi", "IV") + + # Play it twice + play_progression(chords * 2) + + +def blues_in_a(): + """12-bar blues in A.""" + print("12-Bar Blues in A") + print("=" * 40) + print() + + key = Key("A", "major") + I = key.triad(0) + IV = key.triad(3) + V = key.triad(4) + + bars = [I, I, I, I, IV, IV, I, I, V, IV, I, V] + + play_progression(bars, beats_each=1.5) + + +def jazz_ii_v_i(): + """Jazz ii–V–I turnaround through several keys.""" + print("Jazz ii-V-I Turnaround") + print("=" * 40) + print() + + for tonic in ["C", "F", "Bb", "Eb"]: + key = Key(tonic, "major") + chords = key.progression("ii", "V", "I") + print(f" Key of {tonic}:") + play_progression(chords, beats_each=1.5) + print() + + +# ── Main ──────────────────────────────────────────────────────────────── + +SONGS = { + "1": ("Twinkle Twinkle Little Star", twinkle_twinkle), + "2": ("Ode to Joy", ode_to_joy), + "3": ("Happy Birthday", happy_birthday), + "4": ("Fur Elise (opening)", fur_elise), + "5": ("Pop Progression (I-V-vi-IV)", pop_progression), + "6": ("12-Bar Blues in A", blues_in_a), + "7": ("Jazz ii-V-I Turnaround", jazz_ii_v_i), +} if __name__ == "__main__": - play_twinkle() + print("PyTheory Song Player") + print("=" * 40) + print() + + for key, (name, _) in SONGS.items(): + print(f" {key}. {name}") + + print() + choice = input("Pick a song (1-7, or 'all'): ").strip() + + if choice == "all": + for _, (_, fn) in SONGS.items(): + fn() + print() + elif choice in SONGS: + SONGS[choice][1]() + else: + print("Playing all melodies...") + for _, (_, fn) in SONGS.items(): + fn() + print()