mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Rewrite song.py with 10 full arrangements using drums, chords, and synth leads
Bossa nova, bebop, salsa, afrobeat, reggae, funk, 12/8 blues, samba, jazz waltz, and house — each with drum patterns, chord progressions, and hand-written melody lines rendered through the synthesizer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+522
-166
@@ -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!")
|
||||
|
||||
Reference in New Issue
Block a user