From c67a08a34e75fab96b86eb80bdf2280c80b6971d Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 25 Mar 2026 11:23:13 -0400 Subject: [PATCH] Rewrite songs with multi-part API, accept raw float durations - All 10 songs now use score.part() for lead, bass, and chords - Part.add() and .rest() accept raw float beats alongside Duration enums - _RawDuration duck-type wrapper for arbitrary beat values Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/song.py | 745 ++++++++++++++++++++------------------------- pytheory/rhythm.py | 21 +- 2 files changed, 342 insertions(+), 424 deletions(-) diff --git a/examples/song.py b/examples/song.py index 316140f..c7019c4 100644 --- a/examples/song.py +++ b/examples/song.py @@ -1,112 +1,32 @@ -"""Play songs with PyTheory — drums, chords, and synth leads. +"""Play songs with PyTheory — drums, chords, bass, 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.) +Each song uses the multi-part Score API: +- Drum pattern presets (48 genres) +- Named parts with independent synth voices and envelopes +- Chord pads, walking bass lines, and melody leads + +Usage: + python examples/song.py """ -import numpy import sounddevice as sd 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, + Chord, Key, Pattern, Duration, Score, ) +from pytheory.play import play_score, render_score, SAMPLE_RATE -# ── 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.""" +def play_song(score, label=""): + """Render and play. Ctrl-C to skip.""" + if label: + print(f" {label}") + print(f" {score}") + print() try: + buf = render_score(score) sd.play(buf, SAMPLE_RATE) sd.wait() except KeyboardInterrupt: @@ -117,414 +37,393 @@ def play_song(buf): # ── Songs ────────────────────────────────────────────────────────────────── def bossa_nova_girl(): - """Bossa nova in A minor — The Girl from Ipanema vibe.""" + """Bossa nova in A minor — full arrangement.""" print(" Bossa Nova in A minor") - print(" Drums: bossa nova | Chords: i-iv-V7-i | Lead: triangle") - print() + print(" Drums: bossa nova | Lead: triangle | Bass: triangle") - score = Pattern.preset("bossa nova").to_score(repeats=4, bpm=140) + score = Score("4/4", bpm=140) + score.add_pattern(Pattern.preset("bossa nova"), repeats=4) - # 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) + chords = score.part("chords", synth="sine", envelope="pad", volume=0.35) + lead = score.part("lead", synth="triangle", envelope="pluck", volume=0.5) + bass = score.part("bass", synth="triangle", envelope="pluck", volume=0.45) - 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), - ] + for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]: + chords.add(Chord.from_symbol(sym), Duration.WHOLE) - 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) + for n, d in [ + ("E5",.67),("D5",.33),("C5",.67),("B4",.33),("A4",1),("C5",.67),("E5",.33), + ("D5",.67),("C5",.33),("A4",1),(None,1), + ("F5",.67),("E5",.33),("D5",.67),("C5",.33),("D5",1),("F5",.67),("A5",.33), + ("G5",.67),("F5",.33),("D5",1),(None,1), + ("G#5",.67),("F5",.33),("E5",.67),("D5",.33),("E5",1),(None,.5),("B4",.5), + ("D5",.67),("E5",.33),("G#4",1),(None,1), + ("A4",1),("C5",.67),("E5",.33),("A5",1.5),(None,.5), + ("G5",.67),("E5",.33),("C5",.67),("A4",.33),("A4",2), + ]: + lead.rest(d) if n is None else lead.add(n, d) + + for n in ["A2","B2","C3","E3","D3","E3","F3","E3", + "E3","D3","C3","B2","A2","G2","A2","B2", + "A2","B2","C3","E3","D3","E3","F3","E3", + "E3","D3","C3","B2","A2","E2","A2","A2"]: + bass.add(n, Duration.QUARTER) + + play_song(score) def bebop_in_bb(): - """Bebop in Bb — rhythm changes with a horn-like lead.""" + """Bebop in Bb — rhythm changes with horn lead and walking bass.""" print(" Bebop in Bb major") - print(" Drums: bebop | Chords: I-vi-ii-V | Lead: sawtooth") - print() + print(" Drums: bebop | Lead: sawtooth | Bass: triangle") - score = Pattern.preset("bebop").to_score(repeats=8, bpm=160) + score = Score("4/4", bpm=160) + score.add_pattern(Pattern.preset("bebop"), repeats=8) - changes = ["Bb", "Gm", "Cm", "F7"] * 2 - for sym in changes: - score.add(Chord.from_symbol(sym), Duration.WHOLE) + chords = score.part("chords", synth="sine", envelope="piano", volume=0.3) + lead = score.part("lead", synth="saw", envelope="pluck", volume=0.45) + bass = score.part("bass", synth="triangle", envelope="pluck", volume=0.4) - 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), - ] + for sym in ["Bb", "Gm", "Cm", "F7"] * 2: + chords.add(Chord.from_symbol(sym), Duration.WHOLE) - 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) + for n, d in [ + ("Bb4",.67),("D5",.33),("F5",.67),("D5",.33), + ("Bb4",.67),("C5",.33),("D5",.67),("F5",.33), + ("G5",.67),("F5",.33),("D5",.67),("Bb4",.33), + ("A4",.67),("Bb4",.33),("D5",.67),("G4",.33), + ("C5",.67),("Eb5",.33),("G5",.67),("Eb5",.33), + ("C5",.67),("D5",.33),("Eb5",.67),("F5",.33), + ("A5",.67),("G5",.33),("F5",.67),("Eb5",.33), + ("D5",.67),("C5",.33),("A4",.5),(None,.5), + ("Bb4",1),("D5",.67),("F5",.33), + ("G5",.67),("F5",.33),("D5",.67),("Bb4",.33), + ("Bb5",.67),("A5",.33),("G5",.67),("F5",.33), + ("Eb5",.67),("D5",.33),("Bb4",.67),("G4",.33), + ("C5",.5),(None,.5),("Eb5",.67),("G5",.33), + ("F5",.67),("Eb5",.33),("D5",.67),("C5",.33), + ("A4",.67),("C5",.33),("Eb5",.67),("F5",.33), + ("G5",.67),("A5",.33),("Bb5",1), + ]: + lead.rest(d) if n is None else lead.add(n, d) + + for n in ["Bb2","D3","F3","A3","G3","F3","D3","Bb2", + "C3","Eb3","G3","Bb3","F3","A3","C4","Eb3", + "Bb2","D3","F3","A3","G3","F3","D3","Bb2", + "C3","Eb3","G3","Bb3","F3","C3","F2","F3"]: + bass.add(n, Duration.QUARTER) + + play_song(score) 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() + print(" Drums: salsa | Lead: sawtooth | Bass: sine") - score = Pattern.preset("salsa").to_score(repeats=4, bpm=180) + score = Score("4/4", bpm=180) + score.add_pattern(Pattern.preset("salsa"), repeats=4) - changes = ["Em7b5", "A7", "Dm7", "Bbmaj7"] * 2 - for sym in changes: - score.add(Chord.from_symbol(sym), Duration.WHOLE) + chords = score.part("chords", synth="sine", envelope="pad", volume=0.3) + lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4) + bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45) - 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), - ] + for sym in ["Em7b5", "A7", "Dm7", "Bbmaj7"] * 2: + chords.add(Chord.from_symbol(sym), Duration.WHOLE) - 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) + for n, d in [ + ("E5",.67),("G5",.33),("Bb5",.67),("A5",.33), + ("G5",.67),("F5",.33),("E5",.67),("D5",.33), + ("C#5",.67),("D5",.33),("E5",.67),("G5",.33), + ("F5",.67),("E5",.33),("C#5",.5),(None,.5), + ("D5",.5),(None,.17),("F5",.67),("A5",.33), + ("G5",.67),("F5",.33),("E5",.67),("D5",.33), + ("Bb4",1),("D5",.67),("F5",.33),("A5",1),(None,1), + ("E5",.5),("F5",.5),("G5",.67),("A5",.33), + ("Bb5",.67),("A5",.33),("G5",.67),("E5",.33), + ("C#5",.67),("E5",.33),("A5",.67),("G5",.33), + ("F5",.67),("E5",.33),("C#5",.67),("A4",.33), + ("D5",1),("F5",.67),("A5",.33),("G5",.67),("F5",.33),("D5",1),(None,1), + ("Bb4",.67),("D5",.33),("F5",.67),("Bb5",.33),("A5",1.5),(None,.5), + ]: + lead.rest(d) if n is None else lead.add(n, d) + + # Salsa bass: tumbao pattern + for n in ["E2","E3","A2","A3","D2","D3","Bb2","Bb3"] * 4: + bass.add(n, Duration.QUARTER) + + play_song(score) def afrobeat_groove(): - """Afrobeat in E minor — Fela Kuti-inspired groove.""" + """Afrobeat in E minor — Fela Kuti-inspired.""" print(" Afrobeat in E minor") - print(" Drums: afrobeat | Chords: i-iv-bVII-bVI | Lead: saw") - print() + print(" Drums: afrobeat | Lead: sawtooth | Bass: sine") - score = Pattern.preset("afrobeat").to_score(repeats=8, bpm=115) + score = Score("4/4", bpm=115) + score.add_pattern(Pattern.preset("afrobeat"), repeats=8) - # 2 bars per chord, repeat once - changes = ["Em", "Am", "D", "C"] * 2 - for sym in changes: - score.add(Chord.from_symbol(sym), Duration.WHOLE) + chords = score.part("chords", synth="sine", envelope="pad", volume=0.3) + lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4) + bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45) - 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), - ] + for sym in ["Em", "Am", "D", "C"] * 2: + chords.add(Chord.from_symbol(sym), Duration.WHOLE) - 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) + # Hypnotic pentatonic riff + riff = [("E5",.5),("G5",.5),("A5",.5),("G5",.5), + ("E5",.5),("D5",.5),("E5",1), + ("E5",.5),("G5",.5),("A5",.5),("B5",.5), + ("A5",.5),("G5",.5),("E5",1), + (None,.5),("A5",.5),("G5",.5),("E5",.5), + ("D5",1),("E5",.5),("G5",.5), + ("A5",.5),("B5",.5),("A5",.5),("G5",.5), + ("E5",1.5),(None,.5)] + for n, d in riff * 2: + lead.rest(d) if n is None else lead.add(n, d) + + for n in ["E2","E2","G2","A2","A2","G2","E2","D2", + "D2","D2","F#2","A2","C3","C3","B2","G2"] * 2: + bass.add(n, Duration.QUARTER) + + play_song(score) def reggae_one_drop(): - """Reggae one-drop in G major — roots vibes.""" + """Reggae one-drop in G major.""" print(" Reggae One-Drop in G major") - print(" Drums: reggae | Chords: I-IV-V-IV | Lead: triangle") - print() + print(" Drums: reggae | Lead: triangle | Bass: sine") - score = Pattern.preset("reggae").to_score(repeats=8, bpm=80) + score = Score("4/4", bpm=80) + score.add_pattern(Pattern.preset("reggae"), repeats=8) - changes = ["G", "C", "D", "C"] * 2 - for sym in changes: - score.add(Chord.from_symbol(sym), Duration.WHOLE) + chords = score.part("chords", synth="sine", envelope="organ", volume=0.35) + lead = score.part("lead", synth="triangle", envelope="pad", volume=0.45) + bass = score.part("bass", synth="sine", envelope="pluck", volume=0.5) - 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), - ] + for sym in ["G", "C", "D", "C"] * 2: + chords.add(Chord.from_symbol(sym), Duration.WHOLE) - 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) + for n, d in [ + ("G5",1.5),(None,.5),("B5",1),("A5",1), + ("G5",2),("E5",1),("D5",1), + ("C5",1.5),(None,.5),("E5",1),("G5",1), + ("A5",1.5),(None,.5),("G5",2), + ("D5",1.5),(None,.5),("E5",1),("G5",1), + ("A5",2),("B5",1),("A5",1), + ("G5",1.5),(None,.5),("E5",1),("D5",1), + ("G4",3),(None,1), + ]: + lead.rest(d) if n is None else lead.add(n, d) + + for n in ["G2","G2","B2","D3","C3","C3","E3","G3", + "D3","D3","F#3","A3","C3","C3","E3","G3", + "G2","G2","B2","D3","C3","C3","E3","G3", + "D3","D3","F#3","A3","C3","G2","C3","D3"]: + bass.add(n, Duration.QUARTER) + + play_song(score) def funk_workout(): - """Funk in E minor — syncopated 16th note groove.""" + """Funk in E minor — syncopated groove.""" print(" Funk Workout in E minor") - print(" Drums: funk | Chords: i-iv-bVII-V | Lead: saw") - print() + print(" Drums: funk | Lead: sawtooth | Bass: sine") - score = Pattern.preset("funk").to_score(repeats=8, bpm=100) + score = Score("4/4", bpm=100) + score.add_pattern(Pattern.preset("funk"), repeats=8) - changes = ["Em", "Am", "D", "B7"] * 2 - for sym in changes: - score.add(Chord.from_symbol(sym), Duration.WHOLE) + chords = score.part("chords", synth="sine", envelope="staccato", volume=0.3) + lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4) + bass = score.part("bass", synth="sine", envelope="pluck", volume=0.5) - 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), - ] + for sym in ["Em", "Am", "D", "B7"] * 2: + chords.add(Chord.from_symbol(sym), Duration.WHOLE) - 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) + for n, d in [ + ("E5",.25),("E5",.25),(None,.25),("G5",.25), + (None,.25),("A5",.25),("G5",.25),("E5",.25), + ("D5",.5),("E5",.5),(None,.5),("B4",.5), + ("E5",.25),("E5",.25),(None,.25),("G5",.25), + (None,.25),("A5",.25),("B5",.25),("A5",.25), + ("G5",.5),("E5",.5),(None,1), + ("A4",.25),("C5",.25),("E5",.25),("A5",.25), + ("G5",.5),("E5",.5),(None,.5),("C5",.5), + ("A4",.5),("C5",.5),("D5",.5),("E5",.5), + ("E5",1),(None,1), + ("D5",.25),("F#5",.25),("A5",.25),("D5",.25), + ("F#5",.5),("D5",.5),("A4",.5),("D5",.5), + ("D#5",.5),("F#5",.5),("B4",.5),("D#5",.5), + ("F#5",1),(None,1), + ]: + lead.rest(d) if n is None else lead.add(n, d) + + for n in ["E2","E2","G2","E2","A2","A2","C3","A2", + "D2","D2","F#2","D2","B1","B1","D#2","F#2"] * 2: + bass.add(n, Duration.QUARTER) + + play_song(score) def blues_shuffle(): - """12/8 blues in A — slow shuffle with a wailing lead.""" + """12/8 blues in A — slow shuffle.""" print(" 12/8 Blues Shuffle in A") - print(" Drums: 12/8 blues | Chords: I-IV-V | Lead: saw (bluesy)") - print() + print(" Drums: 12/8 blues | Lead: sawtooth | Bass: sine") - score = Pattern.preset("12/8 blues").to_score(repeats=6, bpm=70) + score = Score("12/8", bpm=70) + score.add_pattern(Pattern.preset("12/8 blues"), repeats=6) - # 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) + chords = score.part("chords", synth="sine", envelope="piano", volume=0.35) + lead = score.part("lead", synth="saw", envelope="pluck", volume=0.45) + bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45) - # 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. + for sym in ["A", "A", "D", "D", "E7", "A"]: + chords.add(Chord.from_symbol(sym), Duration.DOTTED_HALF) + chords.add(Chord.from_symbol(sym), Duration.DOTTED_HALF) - 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), - ] + for n, d in [ + ("A4",1),("C5",.67),("A4",.33),("E4",1), + (None,.5),("A4",.5),("C5",.67),("D5",.33), + ("E5",1.5),("D5",.5),("C5",.5),("A4",.5), + ("E4",1.5),(None,.5),("A4",1), + ("D5",1),("F5",.67),("D5",.33),("A4",1), + (None,1),("D5",.67),("F5",.33),("A5",1), + ("G5",.67),("F5",.33),("D5",1),(None,1), + ("E5",.67),("G#4",.33),("B4",.67),("E5",.33), + ("D5",1),("A4",1),(None,1), + ("A4",.67),("C5",.33),("E5",.67),("A5",.33), + ("A5",2),(None,1), + ]: + lead.rest(d) if n is None else lead.add(n, d) - 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) + for n in ["A1","A1","E2","A2","A1","A1","E2","A2","A1","A1","E2","A2", + "D2","D2","A2","D2","D2","D2","A2","D2", + "E2","E2","B2","E2","A1","A1","E2","A2", + "E2","E2","B2","E2","A1","A1","E2","A2"]: + bass.add(n, Duration.QUARTER) + + play_song(score) 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() + print(" Drums: samba | Lead: triangle | Bass: sine") - score = Pattern.preset("samba").to_score(repeats=8, bpm=170) + score = Score("4/4", bpm=170) + score.add_pattern(Pattern.preset("samba"), repeats=8) - changes = ["G", "Em", "Am", "D7"] * 2 - for sym in changes: - score.add(Chord.from_symbol(sym), Duration.WHOLE) + chords = score.part("chords", synth="sine", envelope="pad", volume=0.3) + lead = score.part("lead", synth="triangle", envelope="pluck", volume=0.45) + bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45) - 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), - ] + for sym in ["G", "Em", "Am", "D7"] * 2: + chords.add(Chord.from_symbol(sym), Duration.WHOLE) - 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) + for n, d in [ + ("B5",.33),("A5",.33),("G5",.34),("F#5",.5),("E5",.5), + ("D5",.5),("G5",.5),("B5",.5),("A5",.5), + ("G5",.67),("E5",.33),("D5",.67),("B4",.33),("G4",1),(None,1), + ("E5",.5),("G5",.5),("B5",.5),("A5",.5), + ("G5",.67),("F#5",.33),("E5",.67),("D5",.33),("E5",1),(None,1), + ("A5",.5),("C6",.5),("B5",.5),("A5",.5), + ("G5",.67),("E5",.33),("C5",.67),("A4",.33),("A4",1),(None,1), + ("D5",.5),("F#5",.5),("A5",.5),("C5",.5), + ("B4",.67),("A4",.33),("G4",.67),("F#4",.33),("G4",2),(None,2), + ]: + lead.rest(d) if n is None else lead.add(n, d) + + for n in ["G2","B2","D3","G2","E2","G2","B2","E2", + "A2","C3","E3","A2","D2","F#2","A2","D3"] * 2: + bass.add(n, Duration.QUARTER) + + play_song(score) def jazz_waltz(): - """Jazz waltz in F major — 3/4 time with brushes feel.""" + """Jazz waltz in F major — 3/4 time.""" print(" Jazz Waltz in F major") - print(" Drums: waltz | Chords: I-ii-V-I | Lead: triangle") - print() + print(" Drums: waltz | Lead: triangle | Bass: sine") - score = Pattern.preset("waltz").to_score(repeats=16, bpm=150) + score = Score("3/4", bpm=150) + score.add_pattern(Pattern.preset("waltz"), repeats=16) + + chords = score.part("chords", synth="sine", envelope="pad", volume=0.35) + lead = score.part("lead", synth="triangle", envelope="pluck", volume=0.45) + bass = score.part("bass", synth="sine", envelope="pluck", volume=0.4) - # 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) + for _ in range(4): + chords.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), - ] + for n, d in [ + ("A5",1.5),("G5",.5),("F5",1),("E5",1),("C5",1),("F5",1), + ("A5",2),(None,1),("G5",2),(None,1), + ("Bb5",1),("A5",.5),("G5",.5),("F5",1),("D5",1),("G5",1), + ("Bb5",2),(None,1),("A5",1.5),("G5",.5),("F5",1), + ("E5",1),("G5",1),("Bb5",1),("A5",1.5),("G5",.5),("E5",1), + ("C5",2),(None,1),("E5",1),("G5",1),("C5",1), + ("F5",2),("A5",1),("C6",2),(None,1), + ("A5",1),("F5",1),("C5",1),("F5",3), + ]: + lead.rest(d) if n is None else lead.add(n, d) - 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) + for n in ["F2","A2","C3","G2","Bb2","D3","C2","E2","G2","F2","A2","C3"] * 4: + bass.add(n, Duration.QUARTER) + + play_song(score) def house_anthem(): - """House in C minor — four-on-the-floor with pad chords.""" + """House in C minor — four-on-the-floor with acid lead.""" print(" House Anthem in C minor") - print(" Drums: house | Chords: i-bVI-bVII-i | Lead: saw (acid)") - print() + print(" Drums: house | Lead: sawtooth (acid) | Bass: sine") - score = Pattern.preset("house").to_score(repeats=8, bpm=124) + score = Score("4/4", bpm=124) + score.add_pattern(Pattern.preset("house"), repeats=8) - changes = ["Cm", "Ab", "Bb", "Cm"] * 2 - for sym in changes: - score.add(Chord.from_symbol(sym), Duration.WHOLE) + chords = score.part("chords", synth="sine", envelope="pad", volume=0.35) + lead = score.part("lead", synth="saw", envelope="staccato", volume=0.4) + bass = score.part("bass", synth="sine", envelope="pluck", volume=0.5) - 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), - ] + for sym in ["Cm", "Ab", "Bb", "Cm"] * 2: + chords.add(Chord.from_symbol(sym), Duration.WHOLE) - 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) + # Acid arpeggios + for n, d in [ + ("C5",.25),("Eb5",.25),("G5",.25),("C5",.25), + ("Eb5",.25),("G5",.25),("C5",.25),("Eb5",.25), + ("G5",.25),("Eb5",.25),("C5",.25),("G4",.25), + ("C5",.5),(None,.5),("Eb5",.5),("G5",.5), + ("Ab4",.25),("C5",.25),("Eb5",.25),("Ab4",.25), + ("C5",.25),("Eb5",.25),("Ab4",.25),("C5",.25), + ("Eb5",.25),("C5",.25),("Ab4",.25),("Eb4",.25), + ("Ab4",.5),(None,.5),("C5",.5),("Eb5",.5), + ("Bb4",.25),("D5",.25),("F5",.25),("Bb4",.25), + ("D5",.25),("F5",.25),("Bb4",.25),("D5",.25), + ("F5",.5),("D5",.5),("Bb4",.5),("F5",.5), + ("C5",.25),("Eb5",.25),("G5",.25),("C6",.25), + ("G5",.25),("Eb5",.25),("C5",.25),(None,.25), + ("C5",1.5),(None,.5), + ("G5",.25),("G5",.25),("Eb5",.25),("C5",.25), + ("G5",.25),("G5",.25),("Eb5",.25),("C5",.25), + ("Eb5",.5),("C5",.5),("G4",.5),("C5",.5), + ("Eb5",1),(None,1), + ("Ab4",.5),("C5",.5),("Eb5",.5),("Ab5",.5), + ("G5",.5),("Eb5",.5),("C5",1), + ("Bb4",.5),("D5",.5),("F5",.5),("Bb5",.5), + ("F5",.5),("D5",.5),("Bb4",1), + ("C5",.5),("Eb5",.5),("G5",.5),("C6",.5),("C6",2), + ]: + lead.rest(d) if n is None else lead.add(n, d) + + # Pumping bass + for n in ["C2","C2","C2","C2","Ab1","Ab1","Ab1","Ab1", + "Bb1","Bb1","Bb1","Bb1","C2","C2","C2","C2"] * 2: + bass.add(n, Duration.QUARTER) + + play_song(score) # ── Main ─────────────────────────────────────────────────────────────────── diff --git a/pytheory/rhythm.py b/pytheory/rhythm.py index 757dc81..4943d2a 100644 --- a/pytheory/rhythm.py +++ b/pytheory/rhythm.py @@ -59,8 +59,21 @@ class Note: return self.duration.value -def Rest(duration: Duration = Duration.QUARTER) -> Note: +class _RawDuration: + """A duck-typed Duration wrapper for raw float beat values.""" + __slots__ = ("value",) + + def __init__(self, beats: float): + self.value = float(beats) + + def __repr__(self): + return f"{self.value} beats" + + +def Rest(duration=Duration.QUARTER) -> Note: """Create a rest (silent note) with the given duration.""" + if isinstance(duration, (int, float)): + duration = _RawDuration(duration) return Note(tone=None, duration=duration) @@ -907,16 +920,22 @@ class Part: def add(self, tone_or_string, duration=Duration.QUARTER) -> "Part": """Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``. + Duration can be a ``Duration`` enum or a raw float (beats). + Returns self for chaining. """ if isinstance(tone_or_string, str): from .tones import Tone tone_or_string = Tone.from_string(tone_or_string, system="western") + if isinstance(duration, (int, float)): + duration = _RawDuration(duration) self.notes.append(Note(tone=tone_or_string, duration=duration)) return self def rest(self, duration=Duration.QUARTER) -> "Part": """Add a rest. Returns self for chaining.""" + if isinstance(duration, (int, float)): + duration = _RawDuration(duration) self.notes.append(Note(tone=None, duration=duration)) return self