diff --git a/examples/song.py b/examples/song.py index b14b067..316140f 100644 --- a/examples/song.py +++ b/examples/song.py @@ -1,205 +1,560 @@ -"""Play melodies and chord progressions with PyTheory. +"""Play songs with PyTheory — drums, chords, and synth leads. Requires PortAudio: brew install portaudio (macOS) + +Each song demonstrates a different combination of: +- Drum pattern presets (48 genres available) +- Chord progressions (Roman numeral or symbol-based) +- Monosynth melody lines (sine, saw, triangle waveforms) +- ADSR envelope shaping (piano, pluck, pad, bell, etc.) """ -from pytheory import Tone, Chord, Key, TonedScale, play, Synth +import numpy +import sounddevice as sd -# ── Helpers ───────────────────────────────────────────────────────────── - -BPM = 180 -BEAT = 60_000 // BPM # ms per beat +from pytheory import ( + Tone, Chord, Key, Pattern, Duration, Score, + Synth, Envelope, +) +from pytheory.play import ( + _render, _render_drum_hit, _apply_envelope, + sawtooth_wave, triangle_wave, sine_wave, + SAMPLE_RATE, SAMPLE_PEAK, +) -def play_melody(notes, synth=Synth.SINE): - """Play a sequence of (note_string, beats) tuples.""" +# ── Engine ───────────────────────────────────────────────────────────────── + +def render_score(score, melody=None, melody_synth=sawtooth_wave, + melody_envelope=(0.003, 0.05, 0.6, 0.08), + melody_volume=0.4, chord_volume=0.35, drum_volume=0.5): + """Render a full arrangement to a float32 buffer. + + Args: + score: Score with drum hits and chord notes. + melody: Optional list of (note_string_or_None, beats) tuples. + melody_synth: Wave function for the lead voice. + melody_envelope: (attack, decay, sustain, release) for the lead. + melody_volume: Lead mix level (0-1). + chord_volume: Chord mix level (0-1). + drum_volume: Drum mix level (0-1). + + Returns: + Float32 numpy array of mixed audio. + """ + samples_per_beat = int(SAMPLE_RATE * 60.0 / score.bpm) + total_beats = score.total_beats + + # If melody extends beyond score, expand + if melody: + melody_beats = sum(b for _, b in melody) + total_beats = max(total_beats, melody_beats) + + total_samples = int(total_beats * samples_per_beat) + buf = numpy.zeros(total_samples, dtype=numpy.float32) + + # Chords + beat_pos = 0.0 + for note in score.notes: + if note.tone is not None: + start = int(beat_pos * samples_per_beat) + dur_ms = note.beats * 60_000 / score.bpm + rendered = _render(note.tone, t=dur_ms, envelope=Envelope.PIANO) + rendered_f32 = rendered.astype(numpy.float32) / SAMPLE_PEAK + end = min(start + len(rendered_f32), total_samples) + buf[start:end] += rendered_f32[:end - start] * chord_volume + beat_pos += note.beats + + # Drums + for hit in score._drum_hits: + start = int(hit.position * samples_per_beat) + if start >= total_samples: + continue + remaining = total_samples - start + hit_len = min(int(SAMPLE_RATE * 0.5), remaining) + wave = _render_drum_hit(hit.sound.value, hit_len) + vel_scale = hit.velocity / 127.0 + buf[start:start + hit_len] += wave * vel_scale * drum_volume + + # Melody + if melody: + a, d, s, r = melody_envelope + lead_pos = 0.0 + for note_name, beats in melody: + start = int(lead_pos * samples_per_beat) + dur_ms = beats * 60_000 / score.bpm + n_samples = int(SAMPLE_RATE * dur_ms / 1000) + + if note_name is not None and start + n_samples <= total_samples: + tone = Tone.from_string(note_name, system="western") + hz = tone.pitch() + wave = melody_synth(hz, n_samples=n_samples) + wave_f32 = wave.astype(numpy.float32) / SAMPLE_PEAK + wave_f32 = _apply_envelope(wave_f32, a, d, s, r) + end = min(start + len(wave_f32), total_samples) + buf[start:end] += wave_f32[:end - start] * melody_volume + + lead_pos += beats + + # Normalize + peak = numpy.max(numpy.abs(buf)) + if peak > 0: + buf = buf / peak * 0.9 + + return buf + + +def play_song(buf): + """Play a rendered buffer. Ctrl-C to skip.""" try: - 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)) + sd.play(buf, SAMPLE_RATE) + sd.wait() except KeyboardInterrupt: - print("\n Stopped.") + sd.stop() + print(" (skipped)") -def play_progression(chords, beats_each=2, synth=Synth.SINE): - """Play a list of Chord objects.""" - try: - 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)) - except KeyboardInterrupt: - print("\n Stopped.") +# ── Songs ────────────────────────────────────────────────────────────────── - -# ── Songs ─────────────────────────────────────────────────────────────── - -def twinkle_twinkle(): - """Twinkle Twinkle Little Star — C major.""" - print("Twinkle Twinkle Little Star") - print("=" * 40) - - 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), - ] - - play_melody(melody) - - -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) +def bossa_nova_girl(): + """Bossa nova in A minor — The Girl from Ipanema vibe.""" + print(" Bossa Nova in A minor") + print(" Drums: bossa nova | Chords: i-iv-V7-i | Lead: triangle") print() - key = Key("C", "major") - chords = key.progression("I", "V", "vi", "IV") + score = Pattern.preset("bossa nova").to_score(repeats=4, bpm=140) - # Play it twice - play_progression(chords * 2) + # Am → Dm → E7 → Am (x2) + changes = ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"] + for sym in changes: + score.add(Chord.from_symbol(sym), Duration.WHOLE) + + melody = [ + # Bar 1-2: floating line over Am + ("E5", 0.67), ("D5", 0.33), ("C5", 0.67), ("B4", 0.33), + ("A4", 1.0), ("C5", 0.67), ("E5", 0.33), + ("D5", 0.67), ("C5", 0.33), ("A4", 1.0), (None, 1.0), + # Bar 3-4: reaching up over Dm + ("F5", 0.67), ("E5", 0.33), ("D5", 0.67), ("C5", 0.33), + ("D5", 1.0), ("F5", 0.67), ("A5", 0.33), + ("G5", 0.67), ("F5", 0.33), ("D5", 1.0), (None, 1.0), + # Bar 5-6: tension over E7 + ("G#5", 0.67), ("F5", 0.33), ("E5", 0.67), ("D5", 0.33), + ("E5", 1.0), (None, 0.5), ("B4", 0.5), + ("D5", 0.67), ("E5", 0.33), ("G#4", 1.0), (None, 1.0), + # Bar 7-8: resolve to Am + ("A4", 1.0), ("C5", 0.67), ("E5", 0.33), + ("A5", 1.5), (None, 0.5), + ("G5", 0.67), ("E5", 0.33), ("C5", 0.67), ("A4", 0.33), + ("A4", 2.0), + ] + + buf = render_score(score, melody=melody, melody_synth=triangle_wave, + melody_envelope=(0.01, 0.08, 0.7, 0.15), + melody_volume=0.45) + play_song(buf) -def blues_in_a(): - """12-bar blues in A.""" - print("12-Bar Blues in A") - print("=" * 40) +def bebop_in_bb(): + """Bebop in Bb — rhythm changes with a horn-like lead.""" + print(" Bebop in Bb major") + print(" Drums: bebop | Chords: I-vi-ii-V | Lead: sawtooth") print() - key = Key("A", "major") - I = key.triad(0) - IV = key.triad(3) - V = key.triad(4) + score = Pattern.preset("bebop").to_score(repeats=8, bpm=160) - bars = [I, I, I, I, IV, IV, I, I, V, IV, I, V] + changes = ["Bb", "Gm", "Cm", "F7"] * 2 + for sym in changes: + score.add(Chord.from_symbol(sym), Duration.WHOLE) - play_progression(bars, beats_each=1.5) + melody = [ + # Bar 1: Bb — arpeggio up + ("Bb4", 0.67), ("D5", 0.33), ("F5", 0.67), ("D5", 0.33), + ("Bb4", 0.67), ("C5", 0.33), ("D5", 0.67), ("F5", 0.33), + # Bar 2: Gm — descending with chromatic approach + ("G5", 0.67), ("F5", 0.33), ("D5", 0.67), ("Bb4", 0.33), + ("A4", 0.67), ("Bb4", 0.33), ("D5", 0.67), ("G4", 0.33), + # Bar 3: Cm — climbing + ("C5", 0.67), ("Eb5", 0.33), ("G5", 0.67), ("Eb5", 0.33), + ("C5", 0.67), ("D5", 0.33), ("Eb5", 0.67), ("F5", 0.33), + # Bar 4: F7 — dominant tension + ("A5", 0.67), ("G5", 0.33), ("F5", 0.67), ("Eb5", 0.33), + ("D5", 0.67), ("C5", 0.33), ("A4", 0.5), (None, 0.5), + # Bar 5: Bb — variation + ("Bb4", 1.0), ("D5", 0.67), ("F5", 0.33), + ("G5", 0.67), ("F5", 0.33), ("D5", 0.67), ("Bb4", 0.33), + # Bar 6: Gm — bluesy + ("Bb5", 0.67), ("A5", 0.33), ("G5", 0.67), ("F5", 0.33), + ("Eb5", 0.67), ("D5", 0.33), ("Bb4", 0.67), ("G4", 0.33), + # Bar 7: Cm — syncopated + ("C5", 0.5), (None, 0.5), ("Eb5", 0.67), ("G5", 0.33), + ("F5", 0.67), ("Eb5", 0.33), ("D5", 0.67), ("C5", 0.33), + # Bar 8: F7 — turnaround lick + ("A4", 0.67), ("C5", 0.33), ("Eb5", 0.67), ("F5", 0.33), + ("G5", 0.67), ("A5", 0.33), ("Bb5", 1.0), + ] + + buf = render_score(score, melody=melody, melody_synth=sawtooth_wave, + melody_envelope=(0.003, 0.05, 0.6, 0.08), + melody_volume=0.4) + play_song(buf) -def jazz_ii_v_i(): - """Jazz ii–V–I turnaround through several keys.""" - print("Jazz ii-V-I Turnaround") - print("=" * 40) +def salsa_descarga(): + """Salsa descarga in D minor — clave-driven jam.""" + print(" Salsa Descarga in D minor") + print(" Drums: salsa | Chords: ii-V-i-bVI | Lead: sawtooth") 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() + score = Pattern.preset("salsa").to_score(repeats=4, bpm=180) + + changes = ["Em7b5", "A7", "Dm7", "Bbmaj7"] * 2 + for sym in changes: + score.add(Chord.from_symbol(sym), Duration.WHOLE) + + melody = [ + # Bar 1: Em7b5 — angular line + ("E5", 0.67), ("G5", 0.33), ("Bb5", 0.67), ("A5", 0.33), + ("G5", 0.67), ("F5", 0.33), ("E5", 0.67), ("D5", 0.33), + # Bar 2: A7 — chromatic descent + ("C#5", 0.67), ("D5", 0.33), ("E5", 0.67), ("G5", 0.33), + ("F5", 0.67), ("E5", 0.33), ("C#5", 0.5), (None, 0.5), + # Bar 3: Dm7 — syncopated + ("D5", 0.5), (None, 0.17), ("F5", 0.67), ("A5", 0.33), + ("G5", 0.67), ("F5", 0.33), ("E5", 0.67), ("D5", 0.33), + # Bar 4: Bbmaj7 — resolution + ("Bb4", 1.0), ("D5", 0.67), ("F5", 0.33), + ("A5", 1.0), (None, 1.0), + # Bar 5-8: second pass — variation + ("E5", 0.5), ("F5", 0.5), ("G5", 0.67), ("A5", 0.33), + ("Bb5", 0.67), ("A5", 0.33), ("G5", 0.67), ("E5", 0.33), + ("C#5", 0.67), ("E5", 0.33), ("A5", 0.67), ("G5", 0.33), + ("F5", 0.67), ("E5", 0.33), ("C#5", 0.67), ("A4", 0.33), + ("D5", 1.0), ("F5", 0.67), ("A5", 0.33), + ("G5", 0.67), ("F5", 0.33), ("D5", 1.0), (None, 1.0), + ("Bb4", 0.67), ("D5", 0.33), ("F5", 0.67), ("Bb5", 0.33), + ("A5", 1.5), (None, 0.5), + ] + + buf = render_score(score, melody=melody, melody_synth=sawtooth_wave, + melody_envelope=(0.005, 0.06, 0.5, 0.1), + melody_volume=0.4, drum_volume=0.55) + play_song(buf) -# ── Main ──────────────────────────────────────────────────────────────── +def afrobeat_groove(): + """Afrobeat in E minor — Fela Kuti-inspired groove.""" + print(" Afrobeat in E minor") + print(" Drums: afrobeat | Chords: i-iv-bVII-bVI | Lead: saw") + print() + + score = Pattern.preset("afrobeat").to_score(repeats=8, bpm=115) + + # 2 bars per chord, repeat once + changes = ["Em", "Am", "D", "C"] * 2 + for sym in changes: + score.add(Chord.from_symbol(sym), Duration.WHOLE) + + melody = [ + # Repetitive, hypnotic pentatonic riff + ("E5", 0.5), ("G5", 0.5), ("A5", 0.5), ("G5", 0.5), + ("E5", 0.5), ("D5", 0.5), ("E5", 1.0), + ("E5", 0.5), ("G5", 0.5), ("A5", 0.5), ("B5", 0.5), + ("A5", 0.5), ("G5", 0.5), ("E5", 1.0), + # Variation with syncopation + (None, 0.5), ("A5", 0.5), ("G5", 0.5), ("E5", 0.5), + ("D5", 1.0), ("E5", 0.5), ("G5", 0.5), + ("A5", 0.5), ("B5", 0.5), ("A5", 0.5), ("G5", 0.5), + ("E5", 1.5), (None, 0.5), + # Second half: octave higher accents + ("E5", 0.5), ("G5", 0.5), ("A5", 0.5), ("G5", 0.5), + ("E5", 0.5), ("D5", 0.5), ("B4", 1.0), + ("E5", 0.5), ("G5", 0.5), ("A5", 0.5), ("B5", 0.5), + ("A5", 0.5), ("G5", 0.5), ("E5", 1.0), + (None, 0.5), ("B5", 0.5), ("A5", 0.5), ("G5", 0.5), + ("E5", 1.0), ("D5", 0.5), ("E5", 0.5), + ("E5", 2.0), (None, 2.0), + ] + + buf = render_score(score, melody=melody, melody_synth=sawtooth_wave, + melody_envelope=(0.005, 0.08, 0.5, 0.1), + melody_volume=0.35, drum_volume=0.55) + play_song(buf) + + +def reggae_one_drop(): + """Reggae one-drop in G major — roots vibes.""" + print(" Reggae One-Drop in G major") + print(" Drums: reggae | Chords: I-IV-V-IV | Lead: triangle") + print() + + score = Pattern.preset("reggae").to_score(repeats=8, bpm=80) + + changes = ["G", "C", "D", "C"] * 2 + for sym in changes: + score.add(Chord.from_symbol(sym), Duration.WHOLE) + + melody = [ + # Laid-back pentatonic melody + ("G5", 1.5), (None, 0.5), ("B5", 1.0), ("A5", 1.0), + ("G5", 2.0), ("E5", 1.0), ("D5", 1.0), + ("C5", 1.5), (None, 0.5), ("E5", 1.0), ("G5", 1.0), + ("A5", 1.5), (None, 0.5), ("G5", 2.0), + # Second phrase + ("D5", 1.5), (None, 0.5), ("E5", 1.0), ("G5", 1.0), + ("A5", 2.0), ("B5", 1.0), ("A5", 1.0), + ("G5", 1.5), (None, 0.5), ("E5", 1.0), ("D5", 1.0), + ("G4", 3.0), (None, 1.0), + ] + + buf = render_score(score, melody=melody, melody_synth=triangle_wave, + melody_envelope=(0.02, 0.1, 0.7, 0.2), + melody_volume=0.45, drum_volume=0.45) + play_song(buf) + + +def funk_workout(): + """Funk in E minor — syncopated 16th note groove.""" + print(" Funk Workout in E minor") + print(" Drums: funk | Chords: i-iv-bVII-V | Lead: saw") + print() + + score = Pattern.preset("funk").to_score(repeats=8, bpm=100) + + changes = ["Em", "Am", "D", "B7"] * 2 + for sym in changes: + score.add(Chord.from_symbol(sym), Duration.WHOLE) + + melody = [ + # Funky 16th-note figure + ("E5", 0.25), ("E5", 0.25), (None, 0.25), ("G5", 0.25), + (None, 0.25), ("A5", 0.25), ("G5", 0.25), ("E5", 0.25), + ("D5", 0.5), ("E5", 0.5), (None, 0.5), ("B4", 0.5), + ("E5", 0.25), ("E5", 0.25), (None, 0.25), ("G5", 0.25), + (None, 0.25), ("A5", 0.25), ("B5", 0.25), ("A5", 0.25), + ("G5", 0.5), ("E5", 0.5), (None, 1.0), + # Am phrase + ("A4", 0.25), ("C5", 0.25), ("E5", 0.25), ("A5", 0.25), + ("G5", 0.5), ("E5", 0.5), (None, 0.5), ("C5", 0.5), + ("A4", 0.5), ("C5", 0.5), ("D5", 0.5), ("E5", 0.5), + ("E5", 1.0), (None, 1.0), + # D resolution + ("D5", 0.25), ("F#5", 0.25), ("A5", 0.25), ("D5", 0.25), + ("F#5", 0.5), ("D5", 0.5), ("A4", 0.5), ("D5", 0.5), + # B7 turnaround + ("D#5", 0.5), ("F#5", 0.5), ("B4", 0.5), ("D#5", 0.5), + ("F#5", 1.0), (None, 1.0), + ] + + buf = render_score(score, melody=melody, melody_synth=sawtooth_wave, + melody_envelope=(0.002, 0.03, 0.5, 0.05), + melody_volume=0.4, drum_volume=0.55) + play_song(buf) + + +def blues_shuffle(): + """12/8 blues in A — slow shuffle with a wailing lead.""" + print(" 12/8 Blues Shuffle in A") + print(" Drums: 12/8 blues | Chords: I-IV-V | Lead: saw (bluesy)") + print() + + score = Pattern.preset("12/8 blues").to_score(repeats=6, bpm=70) + + # 12 bars: I I I I IV IV I I V IV I V (each bar = 6 beats in 12/8) + bars = ["A", "A", "A", "A", "D", "D", + "A", "A", "E7", "D", "A", "E7"] + for sym in bars: + score.add(Chord.from_symbol(sym), Duration.DOTTED_HALF) + score.add(Chord.from_symbol(sym), Duration.DOTTED_HALF) + + # Wait, 12 bars * 6 beats = 72 beats, drums = 6 repeats * 6 = 36 beats + # Need to match. Let's just do 6 bars of blues (half a chorus) + # Actually, 6 repeats * 6 beats = 36 beats. 6 bars * 6 = 36. Perfect. + + melody = [ + # Bars 1-2 over A: classic blues opening + ("A4", 1.0), ("C5", 0.67), ("A4", 0.33), ("E4", 1.0), + (None, 0.5), ("A4", 0.5), ("C5", 0.67), ("D5", 0.33), + ("E5", 1.5), ("D5", 0.5), ("C5", 0.5), ("A4", 0.5), + ("E4", 1.5), (None, 0.5), ("A4", 1.0), + # Bars 3-4 over D: reaching + ("D5", 1.0), ("F5", 0.67), ("D5", 0.33), ("A4", 1.0), + (None, 1.0), ("D5", 0.67), ("F5", 0.33), ("A5", 1.0), + ("G5", 0.67), ("F5", 0.33), ("D5", 1.0), (None, 1.0), + # Bars 5-6 over E7/D turnaround + ("E5", 0.67), ("G#4", 0.33), ("B4", 0.67), ("E5", 0.33), + ("D5", 1.0), ("A4", 1.0), (None, 1.0), + ("A4", 0.67), ("C5", 0.33), ("E5", 0.67), ("A5", 0.33), + ("A5", 2.0), (None, 1.0), + ] + + buf = render_score(score, melody=melody, melody_synth=sawtooth_wave, + melody_envelope=(0.01, 0.1, 0.6, 0.2), + melody_volume=0.45, drum_volume=0.45) + play_song(buf) + + +def samba_de_janeiro(): + """Samba in G major — carnival energy.""" + print(" Samba in G major") + print(" Drums: samba | Chords: I-vi-ii-V | Lead: triangle") + print() + + score = Pattern.preset("samba").to_score(repeats=8, bpm=170) + + changes = ["G", "Em", "Am", "D7"] * 2 + for sym in changes: + score.add(Chord.from_symbol(sym), Duration.WHOLE) + + melody = [ + # Fast, rhythmic melody + ("B5", 0.33), ("A5", 0.33), ("G5", 0.34), ("F#5", 0.5), ("E5", 0.5), + ("D5", 0.5), ("G5", 0.5), ("B5", 0.5), ("A5", 0.5), + ("G5", 0.67), ("E5", 0.33), ("D5", 0.67), ("B4", 0.33), + ("G4", 1.0), (None, 1.0), + # Over Em + ("E5", 0.5), ("G5", 0.5), ("B5", 0.5), ("A5", 0.5), + ("G5", 0.67), ("F#5", 0.33), ("E5", 0.67), ("D5", 0.33), + ("E5", 1.0), (None, 1.0), + # Over Am + ("A5", 0.5), ("C6", 0.5), ("B5", 0.5), ("A5", 0.5), + ("G5", 0.67), ("E5", 0.33), ("C5", 0.67), ("A4", 0.33), + ("A4", 1.0), (None, 1.0), + # Over D7 — turnaround + ("D5", 0.5), ("F#5", 0.5), ("A5", 0.5), ("C5", 0.5), + ("B4", 0.67), ("A4", 0.33), ("G4", 0.67), ("F#4", 0.33), + ("G4", 2.0), (None, 2.0), + ] + + buf = render_score(score, melody=melody, melody_synth=triangle_wave, + melody_envelope=(0.005, 0.05, 0.6, 0.1), + melody_volume=0.4, drum_volume=0.5) + play_song(buf) + + +def jazz_waltz(): + """Jazz waltz in F major — 3/4 time with brushes feel.""" + print(" Jazz Waltz in F major") + print(" Drums: waltz | Chords: I-ii-V-I | Lead: triangle") + print() + + score = Pattern.preset("waltz").to_score(repeats=16, bpm=150) + + # 4 bars per change, 3 beats each = 12 beats per chord + for _ in range(2): + for sym in ["Fmaj7", "Gm", "C7", "Fmaj7"]: + score.add(Chord.from_symbol(sym), Duration.DOTTED_HALF) + score.add(Chord.from_symbol(sym), Duration.DOTTED_HALF) + score.add(Chord.from_symbol(sym), Duration.DOTTED_HALF) + score.add(Chord.from_symbol(sym), Duration.DOTTED_HALF) + + melody = [ + # 3/4 phrases, lyrical + ("A5", 1.5), ("G5", 0.5), ("F5", 1.0), + ("E5", 1.0), ("C5", 1.0), ("F5", 1.0), + ("A5", 2.0), (None, 1.0), + ("G5", 2.0), (None, 1.0), + # Over Gm + ("Bb5", 1.0), ("A5", 0.5), ("G5", 0.5), + ("F5", 1.0), ("D5", 1.0), ("G5", 1.0), + ("Bb5", 2.0), (None, 1.0), + ("A5", 1.5), ("G5", 0.5), ("F5", 1.0), + # Over C7 + ("E5", 1.0), ("G5", 1.0), ("Bb5", 1.0), + ("A5", 1.5), ("G5", 0.5), ("E5", 1.0), + ("C5", 2.0), (None, 1.0), + ("E5", 1.0), ("G5", 1.0), ("C5", 1.0), + # Resolve to F + ("F5", 2.0), ("A5", 1.0), + ("C6", 2.0), (None, 1.0), + ("A5", 1.0), ("F5", 1.0), ("C5", 1.0), + ("F5", 3.0), + ] + + buf = render_score(score, melody=melody, melody_synth=triangle_wave, + melody_envelope=(0.02, 0.1, 0.7, 0.2), + melody_volume=0.45, drum_volume=0.4) + play_song(buf) + + +def house_anthem(): + """House in C minor — four-on-the-floor with pad chords.""" + print(" House Anthem in C minor") + print(" Drums: house | Chords: i-bVI-bVII-i | Lead: saw (acid)") + print() + + score = Pattern.preset("house").to_score(repeats=8, bpm=124) + + changes = ["Cm", "Ab", "Bb", "Cm"] * 2 + for sym in changes: + score.add(Chord.from_symbol(sym), Duration.WHOLE) + + melody = [ + # Acid-style arpeggiated lead + ("C5", 0.25), ("Eb5", 0.25), ("G5", 0.25), ("C5", 0.25), + ("Eb5", 0.25), ("G5", 0.25), ("C5", 0.25), ("Eb5", 0.25), + ("G5", 0.25), ("Eb5", 0.25), ("C5", 0.25), ("G4", 0.25), + ("C5", 0.5), (None, 0.5), ("Eb5", 0.5), ("G5", 0.5), + # Over Ab + ("Ab4", 0.25), ("C5", 0.25), ("Eb5", 0.25), ("Ab4", 0.25), + ("C5", 0.25), ("Eb5", 0.25), ("Ab4", 0.25), ("C5", 0.25), + ("Eb5", 0.25), ("C5", 0.25), ("Ab4", 0.25), ("Eb4", 0.25), + ("Ab4", 0.5), (None, 0.5), ("C5", 0.5), ("Eb5", 0.5), + # Over Bb + ("Bb4", 0.25), ("D5", 0.25), ("F5", 0.25), ("Bb4", 0.25), + ("D5", 0.25), ("F5", 0.25), ("Bb4", 0.25), ("D5", 0.25), + ("F5", 0.5), ("D5", 0.5), ("Bb4", 0.5), ("F5", 0.5), + # Back to Cm — resolution + ("C5", 0.25), ("Eb5", 0.25), ("G5", 0.25), ("C6", 0.25), + ("G5", 0.25), ("Eb5", 0.25), ("C5", 0.25), (None, 0.25), + ("C5", 1.5), (None, 0.5), + # Second half: more intense + ("G5", 0.25), ("G5", 0.25), ("Eb5", 0.25), ("C5", 0.25), + ("G5", 0.25), ("G5", 0.25), ("Eb5", 0.25), ("C5", 0.25), + ("Eb5", 0.5), ("C5", 0.5), ("G4", 0.5), ("C5", 0.5), + ("Eb5", 1.0), (None, 1.0), + ("Ab4", 0.5), ("C5", 0.5), ("Eb5", 0.5), ("Ab5", 0.5), + ("G5", 0.5), ("Eb5", 0.5), ("C5", 1.0), + ("Bb4", 0.5), ("D5", 0.5), ("F5", 0.5), ("Bb5", 0.5), + ("F5", 0.5), ("D5", 0.5), ("Bb4", 1.0), + ("C5", 0.5), ("Eb5", 0.5), ("G5", 0.5), ("C6", 0.5), + ("C6", 2.0), + ] + + buf = render_score(score, melody=melody, melody_synth=sawtooth_wave, + melody_envelope=(0.001, 0.03, 0.4, 0.05), + melody_volume=0.35, chord_volume=0.4, drum_volume=0.5) + play_song(buf) + + +# ── 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), + "1": ("Bossa Nova in A minor", bossa_nova_girl), + "2": ("Bebop in Bb major", bebop_in_bb), + "3": ("Salsa Descarga in D minor", salsa_descarga), + "4": ("Afrobeat in E minor", afrobeat_groove), + "5": ("Reggae One-Drop in G major", reggae_one_drop), + "6": ("Funk Workout in E minor", funk_workout), + "7": ("12/8 Blues Shuffle in A", blues_shuffle), + "8": ("Samba in G major", samba_de_janeiro), + "9": ("Jazz Waltz in F major", jazz_waltz), + "10": ("House Anthem in C minor", house_anthem), } if __name__ == "__main__": try: - print("PyTheory Song Player") - print("=" * 40) + print() + print(" PyTheory Song Player") + print(" " + "=" * 40) print() for key, (name, _) in SONGS.items(): - print(f" {key}. {name}") + print(f" {key:>2}. {name}") print() - choice = input("Pick a song (1-7, or 'all'): ").strip() + choice = input(" Pick a song (1-10, or 'all'): ").strip() + print() if choice == "all": for _, (_, fn) in SONGS.items(): @@ -208,9 +563,10 @@ if __name__ == "__main__": elif choice in SONGS: SONGS[choice][1]() else: - print("Playing all melodies...") + print(" Playing all songs...") for _, (_, fn) in SONGS.items(): fn() print() except KeyboardInterrupt: - print("\n\nBye!") + sd.stop() + print("\n\n Bye!")