diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c993ad..445b59a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 2026-04-02 +- **Track 17: Waveforms** — F minor, 118 BPM. Percussive synth blips stacking (sine→triangle→square→saw), occasional sustained pads, 808 sub, rhythmic drums with ghost notes. +- **Track 16: Intrusive** — Bb minor, 92 BPM. Repeating saw synth phrase, Rhodes fights it, drums fight it, acceptance with sub bass and cello. +- **Track 15: Voices** — F# minor, 65 BPM. Five vocal parts multiplying, piano as reality. - **Track 14: An Exception Occurred** — Eb major→minor→major, 80 BPM. Piano arc through stability, spiritual seeking (tambura, sitar, om chant, tabla, tingsha), psychosis (wild theremin ±5 semitone bends, chaos drums, sub bass), despair, hymn (pipe organ), recovery (cello), gratitude. - **Track 13: Sleight of Hand** — D minor, 100 BPM. Nine genre shifts in 72 bars. The 303 through the choir is the moment. - **Track 12: Gravity** — C minor, 88 BPM. Hip hop with sparse piano, 808, boom bap + trap hats. Eastern touches: buried tambura, singing bowl bookends, sitar bend in breakdown. diff --git a/README.md b/README.md index 0442bdb..f115b67 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ Each track is a `.py` file. Run it to hear it. 13. **Sleight of Hand** — D minor, 100 BPM. Music box → didgeridoo → jazz piano → 808 drop → solo theremin → choir → acid 303 rips through the choir → music box returns over boom bap. You never see the next move coming. 14. **An Exception Occurred** — Eb major→minor→major, 80 BPM. Piano-driven arc: stability → spiritual seeking (tambura, sitar, om chant) → psychosis (wild theremin, chaos drums) → despair → hymn (pipe organ) → recovery. Every note by hand. 15. **Voices** — F# minor, 65 BPM. Five vocal parts multiplying across the stereo field. Piano enters as reality. One last whisper, then silence. -16. **Intrusive** — Bb minor, 92 BPM. One saw synth phrase repeating. Rhodes tries to play something else. Drums try to drown it. It gets louder. Stop fighting — acceptance. The thought slows, spaces open, sub bass enters as ground. It passes. +16. **Intrusive** — Bb minor, 92 BPM. One saw synth phrase repeating. Rhodes tries to play something else. Drums try to drown it. Stop fighting — acceptance, sub bass, cello. It passes. +17. **Waveforms** — F minor, 118 BPM. Pure synthesis showcase. Sine, triangle, square, saw — percussive blips layering one by one. Occasional sustained pads open the waveforms up. 808 sub underneath, drums at bar 33, sine melody at the peak. ## Usage diff --git a/play.py b/play.py index 69006b0..c4d2258 100644 --- a/play.py +++ b/play.py @@ -55,6 +55,7 @@ ALBUM_ORDER = [ "an_exception_occurred.py", "voices.py", "intrusive.py", + "waveforms.py", ] diff --git a/tracks/waveforms.py b/tracks/waveforms.py new file mode 100644 index 0000000..c7e43a8 --- /dev/null +++ b/tracks/waveforms.py @@ -0,0 +1,596 @@ +""" +WAVEFORMS — harmonic, melodic, percussive. +Synth blips in open space. Each waveform sings. +Light reverb, clean lines, rhythm from the notes themselves. +A minor, 118 BPM. +""" + +from pytheory import Key, Duration, Score, Tone, play_score +from pytheory.rhythm import DrumSound + +key = Key("F", "minor") +s = key.scale # F G Ab Bb C Db Eb + +F = s[0]; G = s[1]; Ab = s[2]; Bb = s[3] +C = s[4]; Db = s[5]; Eb = s[6] + +score = Score("4/4", bpm=118) + +K = DrumSound.KICK +CH = DrumSound.CLOSED_HAT + +prog = key.progression("i", "VII", "VI", "iv") + +# ═══════════════════════════════════════════════════════════════════ +# STRUCTURE (64 bars, ~3:15): +# Bars 1-8: Sine blips — percussive, melodic, setting the grid +# Bars 9-16: Triangle joins — harmony, counterpoint +# Bars 17-24: Square enters — rhythmic fill, staccato +# Bars 25-32: Saw arp — the energy lifts +# Bars 33-40: All four interlocking — the machine hums +# Bars 41-48: Supersaw pad + kick — the room opens +# Bars 49-56: Peak — everything singing, melodic peak +# Bars 57-64: Unwind — layers peel away, sine last +# ═══════════════════════════════════════════════════════════════════ + +# ── SINE — percussive blips, the seed of everything ─────────── +sine = score.part("sine", synth="sine", envelope="pluck", volume=0.5, + reverb=0.4, reverb_type="taj_mahal", + delay=0.15, delay_time=0.254, delay_feedback=0.25, + pan=-0.15) + +# Bars 1-8: melodic blips — the rhythm IS the melody +# F minor: descending, dark. Land on Ab and Db, not C and F. +sine_phrase = [ + (F, Duration.SIXTEENTH, 78), (None, Duration.SIXTEENTH, 0), + (Eb, Duration.SIXTEENTH, 72), (Db, Duration.SIXTEENTH, 70), + (None, Duration.EIGHTH, 0), + (C, Duration.EIGHTH, 68), + (Db, Duration.SIXTEENTH, 72), (None, Duration.SIXTEENTH, 0), + (Ab.add(-12), Duration.SIXTEENTH, 65), (None, Duration.SIXTEENTH, 0), + (None, Duration.QUARTER, 0), +] +sine_phrase_b = [ + (Ab, Duration.SIXTEENTH, 75), (None, Duration.SIXTEENTH, 0), + (G, Duration.SIXTEENTH, 68), (F, Duration.SIXTEENTH, 72), + (None, Duration.EIGHTH, 0), + (Eb, Duration.EIGHTH, 70), + (Db, Duration.SIXTEENTH, 72), (None, Duration.SIXTEENTH, 0), + (C, Duration.SIXTEENTH, 65), (None, Duration.SIXTEENTH, 0), + (None, Duration.QUARTER, 0), +] +for _ in range(4): + for note, dur, vel in sine_phrase: + if note is None: + sine.rest(dur) + else: + sine.add(note, dur, velocity=vel) + for note, dur, vel in sine_phrase_b: + if note is None: + sine.rest(dur) + else: + sine.add(note, dur, velocity=vel) + +# ── SINE PAD — the same waveform, but sustained, opens up ────── +sine_pad = score.part("sine_pad", synth="sine", envelope="pad", volume=0.2, + reverb=0.3, reverb_type="taj_mahal", + chorus=0.2, chorus_rate=0.06, chorus_depth=0.008, + pan=0.15) + +# Bars 1-4: silent — let the blips establish +for _ in range(4): + sine_pad.rest(Duration.WHOLE) + +# Bars 5-8: sine opens up — held notes an octave below the blips +sine_pad.add(F.add(-12), Duration.WHOLE, velocity=35) +sine_pad.add(Ab.add(-12), Duration.WHOLE, velocity=38) +sine_pad.add(Db, Duration.WHOLE, velocity=40) +sine_pad.add(F.add(-12), Duration.WHOLE, velocity=35) + +# Bars 9-16: wider — octave spread, low and high together +sine_pad.add(F.add(-12), Duration.HALF, velocity=42) +sine_pad.add(Ab, Duration.HALF, velocity=38) +sine_pad.add(Db.add(-12), Duration.WHOLE, velocity=40) +sine_pad.add(F.add(-12), Duration.HALF, velocity=42) +sine_pad.add(C, Duration.HALF, velocity=35) +sine_pad.add(Ab.add(-12), Duration.WHOLE, velocity=40) +sine_pad.add(Db.add(-12), Duration.HALF, velocity=42) +sine_pad.add(Eb, Duration.HALF, velocity=38) +sine_pad.add(F.add(-12), Duration.WHOLE, velocity=40) +sine_pad.add(Ab.add(-12), Duration.WHOLE, velocity=38) + +# Bars 17-56: continues — swelling and receding +for _ in range(10): + sine_pad.add(F.add(-12), Duration.WHOLE, velocity=42) + sine_pad.add(Ab.add(-12), Duration.WHOLE, velocity=38) + sine_pad.add(Db.add(-12), Duration.WHOLE, velocity=40) + sine_pad.add(Eb.add(-12), Duration.WHOLE, velocity=38) + +# Bars 57-64: fading +for vel in [35, 30, 25, 20, 15, 10, 5, 0]: + if vel > 0: + sine_pad.add(F.add(-12), Duration.WHOLE, velocity=vel) + else: + sine_pad.rest(Duration.WHOLE) + +# ── TRIANGLE PAD — occasional sustained warmth ───────────────── +tri_pad = score.part("tri_pad", synth="triangle", envelope="pad", volume=0.15, + reverb=0.25, reverb_type="cathedral", + chorus=0.2, chorus_rate=0.05, chorus_depth=0.01, + pan=-0.35) + +# Mostly silent — just opens up at transitions +for _ in range(16): + tri_pad.rest(Duration.WHOLE) +# Bars 17-18: triangle opens up when square enters +tri_pad.add(Ab.add(-12), Duration.WHOLE, velocity=38) +tri_pad.add(Db, Duration.WHOLE, velocity=35) +for _ in range(6): + tri_pad.rest(Duration.WHOLE) +# Bars 25-26: opens again when saw enters +tri_pad.add(F.add(-12), Duration.WHOLE, velocity=40) +tri_pad.add(Eb, Duration.WHOLE, velocity=35) +for _ in range(6): + tri_pad.rest(Duration.WHOLE) +# Bars 33-34: opens when kick drops +tri_pad.add(Db, Duration.WHOLE, velocity=42) +tri_pad.add(Ab.add(-12), Duration.WHOLE, velocity=38) +for _ in range(14): + tri_pad.rest(Duration.WHOLE) +# Bars 49-50: opens at peak +tri_pad.add(F.add(-12), Duration.WHOLE, velocity=45) +tri_pad.add(Ab.add(-12), Duration.WHOLE, velocity=42) +for _ in range(14): + tri_pad.rest(Duration.WHOLE) + +# ── SAW PAD — rare, big, only at the peak ───────────────────── +saw_pad = score.part("saw_pad", synth="saw", envelope="pad", volume=0.12, + lowpass=2000, + reverb=0.2, reverb_type="spring", + chorus=0.3, chorus_rate=0.1, chorus_depth=0.008, + pan=0.35) + +# Silent until bar 49 +for _ in range(48): + saw_pad.rest(Duration.WHOLE) +# Bars 49-52: the saw opens up — one sustained moment +for chord in prog: + saw_pad.add(chord, Duration.WHOLE, velocity=40) +# Back to silence +for _ in range(12): + saw_pad.rest(Duration.WHOLE) + +# Bars 9-56: blips continue — the constant thread +for _ in range(24): + for note, dur, vel in sine_phrase: + if note is None: + sine.rest(dur) + else: + sine.add(note, dur, velocity=vel) + for note, dur, vel in sine_phrase_b: + if note is None: + sine.rest(dur) + else: + sine.add(note, dur, velocity=vel) + +# Bars 57-64: fading +for rep in range(4): + off = rep * -12 + for note, dur, vel in sine_phrase: + if note is None: + sine.rest(dur) + else: + sine.add(note, dur, velocity=max(20, vel + off)) + for note, dur, vel in sine_phrase_b: + if note is None: + sine.rest(dur) + else: + sine.add(note, dur, velocity=max(20, vel + off)) + +# ── TRIANGLE — harmony, enters bar 9 ───────────────────────── +tri = score.part("triangle", synth="triangle", envelope="pluck", volume=0.4, + reverb=0.2, reverb_decay=1.0, + delay=0.12, delay_time=0.254, delay_feedback=0.2, + pan=0.3) + +for _ in range(8): + tri.rest(Duration.WHOLE) + +# Bars 9-16: counterpoint — fills the gaps in sine's rhythm +# Triangle: descending thirds, landing on dark notes +tri_phrase = [ + (None, Duration.EIGHTH, 0), + (Db, Duration.SIXTEENTH, 70), (C, Duration.SIXTEENTH, 65), + (None, Duration.EIGHTH, 0), + (Ab.add(-12), Duration.SIXTEENTH, 68), (None, Duration.SIXTEENTH, 0), + (Bb.add(-12), Duration.SIXTEENTH, 62), (None, Duration.SIXTEENTH, 0), + (None, Duration.EIGHTH, 0), + (Ab.add(-12), Duration.EIGHTH, 65), +] +tri_phrase_b = [ + (None, Duration.EIGHTH, 0), + (F, Duration.SIXTEENTH, 72), (Eb, Duration.SIXTEENTH, 68), + (None, Duration.EIGHTH, 0), + (Db, Duration.SIXTEENTH, 70), (None, Duration.SIXTEENTH, 0), + (C, Duration.SIXTEENTH, 62), (None, Duration.SIXTEENTH, 0), + (None, Duration.EIGHTH, 0), + (Db, Duration.EIGHTH, 68), +] +for _ in range(4): + for note, dur, vel in tri_phrase: + if note is None: + tri.rest(dur) + else: + tri.add(note, dur, velocity=vel) + for note, dur, vel in tri_phrase_b: + if note is None: + tri.rest(dur) + else: + tri.add(note, dur, velocity=vel) + +# Bars 17-56: continues +for _ in range(20): + for note, dur, vel in tri_phrase: + if note is None: + tri.rest(dur) + else: + tri.add(note, dur, velocity=vel) + for note, dur, vel in tri_phrase_b: + if note is None: + tri.rest(dur) + else: + tri.add(note, dur, velocity=vel) + +# Bars 57-64: fading +for rep in range(4): + off = rep * -12 + for note, dur, vel in tri_phrase: + if note is None: + tri.rest(dur) + else: + tri.add(note, dur, velocity=max(18, vel + off)) + for note, dur, vel in tri_phrase_b: + if note is None: + tri.rest(dur) + else: + tri.add(note, dur, velocity=max(18, vel + off)) + +# ── SQUARE — staccato rhythm, enters bar 17 ────────────────── +sq = score.part("square", synth="square", envelope="pluck", volume=0.3, + lowpass=3500, + reverb=0.15, reverb_decay=0.8, + delay=0.1, delay_time=0.127, delay_feedback=0.15, + pan=-0.4) + +for _ in range(16): + sq.rest(Duration.WHOLE) + +# Bars 17-24: rhythmic stabs — off the beat +sq_stab = [ +# Square: off-beat stabs — Db and Ab, the minor anchors + (None, Duration.SIXTEENTH, 0), (Db, Duration.SIXTEENTH, 70), + (None, Duration.SIXTEENTH, 0), (None, Duration.SIXTEENTH, 0), + (None, Duration.SIXTEENTH, 0), (Ab.add(-12), Duration.SIXTEENTH, 65), + (None, Duration.SIXTEENTH, 0), (Db, Duration.SIXTEENTH, 72), + (None, Duration.EIGHTH, 0), + (None, Duration.SIXTEENTH, 0), (Ab.add(-12), Duration.SIXTEENTH, 65), + (None, Duration.QUARTER, 0), +] +for _ in range(8): + for note, dur, vel in sq_stab: + if note is None: + sq.rest(dur) + else: + sq.add(note, dur, velocity=vel) + +# Bars 25-56: continues +for _ in range(32): + for note, dur, vel in sq_stab: + if note is None: + sq.rest(dur) + else: + sq.add(note, dur, velocity=vel) + +# Bars 57-64: fading +for rep in range(8): + off = rep * -8 + for note, dur, vel in sq_stab: + if note is None: + sq.rest(dur) + else: + sq.add(note, dur, velocity=max(15, vel + off)) + +# ── SAW ARP — energy lift, enters bar 25 ───────────────────── +saw = score.part("saw", synth="saw", envelope="pluck", volume=0.3, + lowpass=4000, + distortion=0.05, saturation=0.2, + reverb=0.18, reverb_type="spring", + delay=0.2, delay_time=0.254, delay_feedback=0.3, + pan=0.4) +saw.lfo("lowpass", rate=0.01, min=2000, max=6000, bars=64, shape="triangle") + +for _ in range(24): + saw.rest(Duration.WHOLE) + +# Bars 25-32: arpeggio — the energy lifts +# Saw arps — descending shapes, minor triads +arp_a = [C, Ab, F, Ab, C, F, Ab, F] # i — Fm descending feel +arp_b = [Bb, G, Eb, G, Bb, Eb, G, Eb] # VII — Eb +arp_c = [Ab, F, Db, F, Ab, Db, F.add(-12), Db.add(-12)] # VI — Db +arp_d = [F, Db, Bb.add(-12), Db, F, Bb.add(-12), Db, F] # iv — Bbm + +for _ in range(2): + for note in arp_a: + saw.add(note, Duration.EIGHTH, velocity=68) + for note in arp_b: + saw.add(note, Duration.EIGHTH, velocity=65) + for note in arp_c: + saw.add(note, Duration.EIGHTH, velocity=70) + for note in arp_d: + saw.add(note, Duration.EIGHTH, velocity=68) + +# Bars 33-56: continues — the constant motion +for _ in range(6): + for note in arp_a: + saw.add(note, Duration.EIGHTH, velocity=72) + for note in arp_b: + saw.add(note, Duration.EIGHTH, velocity=68) + for note in arp_c: + saw.add(note, Duration.EIGHTH, velocity=72) + for note in arp_d: + saw.add(note, Duration.EIGHTH, velocity=70) + +# Bars 57-64: fading arps +for rep in range(2): + off = rep * -15 + for note in arp_a: + saw.add(note, Duration.EIGHTH, velocity=max(20, 62 + off)) + for note in arp_b: + saw.add(note, Duration.EIGHTH, velocity=max(20, 58 + off)) + for note in arp_c: + saw.add(note, Duration.EIGHTH, velocity=max(20, 60 + off)) + for note in arp_d: + saw.add(note, Duration.EIGHTH, velocity=max(20, 58 + off)) + +# ── SUPERSAW PAD — the room opens, bar 41 ──────────────────── +pad = score.part("pad", synth="supersaw", envelope="pad", volume=0.12, + reverb=0.6, reverb_type="taj_mahal", + chorus=0.3, chorus_rate=0.15, chorus_depth=0.008, + lowpass=2500) + +for _ in range(40): + pad.rest(Duration.WHOLE) + +# Bars 41-56: chord pads — warmth behind the blips +for _ in range(4): + for chord in prog: + pad.add(chord, Duration.WHOLE, velocity=48) + +# Bars 57-64: fading +for vel in [42, 35, 28, 22, 15, 10, 5, 0]: + if vel > 0: + pad.add(prog[0], Duration.WHOLE, velocity=vel) + else: + pad.rest(Duration.WHOLE) + +# ── 808 — sustained sub, the grit underneath ────────────────── +sub = score.part("808", synth="sine", envelope="pad", volume=0.55, + lowpass=200, distortion=0.2, distortion_drive=3.0, + sub_osc=0.5, saturation=0.4, sidechain=0.3) + +for _ in range(16): + sub.rest(Duration.WHOLE) + +# Bars 17-56: sustained 808, follows the chord roots +roots = [F.add(-24), Eb.add(-24), Db.add(-24), Bb.add(-24)] +for _ in range(10): + for root in roots: + sub.add(root, Duration.WHOLE, velocity=38) + +# Bars 57-64: fading +for vel in [32, 28, 22, 18, 14, 10, 5, 0]: + if vel > 0: + sub.add(F.add(-24), Duration.WHOLE, velocity=vel) + else: + sub.rest(Duration.WHOLE) + +# ── KICK — four on the floor first, alone ────────────────────── +kick = score.part("kick", volume=0.6, humanize=0.03) + +for _ in range(24): + kick.rest(Duration.WHOLE) + +# Bars 25-32: just the kick — four on the floor, nothing else +for _ in range(8): + for beat in range(4): + kick.hit(K, Duration.QUARTER, velocity=100) + +# Bars 33-56: continues under the full drums +for _ in range(24): + for beat in range(4): + kick.hit(K, Duration.QUARTER, velocity=105) + +# Bars 57-64: fading +for vel in [92, 78, 65, 52, 38, 25, 0, 0]: + if vel > 0: + for beat in range(4): + kick.hit(K, Duration.QUARTER, velocity=vel) + else: + kick.rest(Duration.WHOLE) + +# ── DRUMS — complex rhythmic pattern, enters bar 33 ─────────── +S = DrumSound.SNARE +OH = DrumSound.OPEN_HAT + +hats = score.part("hats", volume=0.25, pan=0.15, humanize=0.04, + delay=0.06, delay_time=0.127, delay_feedback=0.1) + +snare = score.part("snare", volume=0.4, humanize=0.04, + reverb=0.15, reverb_decay=0.6, + delay=0.05, delay_time=0.254, delay_feedback=0.08, + pan=0.05) + +for _ in range(32): + hats.rest(Duration.WHOLE) + snare.rest(Duration.WHOLE) + +# Bars 33-40: rhythmic hats + snare on 2&4 — the groove locks +for _ in range(8): + # Hats: 16ths with accents and ghost notes + hats.hit(CH, Duration.SIXTEENTH, velocity=72) + hats.hit(CH, Duration.SIXTEENTH, velocity=38) + hats.hit(CH, Duration.SIXTEENTH, velocity=55) + hats.hit(CH, Duration.SIXTEENTH, velocity=35) + hats.hit(CH, Duration.SIXTEENTH, velocity=68) + hats.hit(CH, Duration.SIXTEENTH, velocity=40) + hats.hit(OH, Duration.SIXTEENTH, velocity=58) + hats.hit(CH, Duration.SIXTEENTH, velocity=35) + hats.hit(CH, Duration.SIXTEENTH, velocity=72) + hats.hit(CH, Duration.SIXTEENTH, velocity=38) + hats.hit(CH, Duration.SIXTEENTH, velocity=52) + hats.hit(CH, Duration.SIXTEENTH, velocity=35) + hats.hit(CH, Duration.SIXTEENTH, velocity=65) + hats.hit(CH, Duration.SIXTEENTH, velocity=42) + hats.hit(OH, Duration.SIXTEENTH, velocity=55) + hats.hit(CH, Duration.SIXTEENTH, velocity=35) + # Snare: 2 and 4 with ghost notes + snare.rest(Duration.EIGHTH) + snare.hit(S, Duration.SIXTEENTH, velocity=40) # ghost + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.QUARTER, velocity=95) # 2 + snare.rest(Duration.EIGHTH) + snare.hit(S, Duration.SIXTEENTH, velocity=38) # ghost + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.QUARTER, velocity=98) # 4 + +# Bars 41-48: tighter — more ghost notes, busier +for _ in range(8): + hats.hit(CH, Duration.SIXTEENTH, velocity=75) + hats.hit(CH, Duration.SIXTEENTH, velocity=42) + hats.hit(CH, Duration.SIXTEENTH, velocity=58) + hats.hit(CH, Duration.SIXTEENTH, velocity=38) + hats.hit(OH, Duration.SIXTEENTH, velocity=62) + hats.hit(CH, Duration.SIXTEENTH, velocity=35) + hats.hit(CH, Duration.SIXTEENTH, velocity=55) + hats.hit(CH, Duration.SIXTEENTH, velocity=42) + hats.hit(CH, Duration.SIXTEENTH, velocity=72) + hats.hit(CH, Duration.SIXTEENTH, velocity=40) + hats.hit(CH, Duration.SIXTEENTH, velocity=58) + hats.hit(CH, Duration.SIXTEENTH, velocity=35) + hats.hit(OH, Duration.SIXTEENTH, velocity=60) + hats.hit(CH, Duration.SIXTEENTH, velocity=38) + hats.hit(CH, Duration.SIXTEENTH, velocity=55) + hats.hit(CH, Duration.SIXTEENTH, velocity=42) + # Snare with more ghosts + snare.hit(S, Duration.SIXTEENTH, velocity=35) # ghost + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.SIXTEENTH, velocity=38) # ghost + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.QUARTER, velocity=98) # 2 + snare.hit(S, Duration.SIXTEENTH, velocity=35) # ghost + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.SIXTEENTH, velocity=40) # ghost + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.QUARTER, velocity=100) # 4 + +# Bars 49-56: peak — snare fills every 4 bars +for bar in range(8): + if bar % 4 == 3: + # Fill — 16th note snare roll + for i in range(16): + snare.hit(S, Duration.SIXTEENTH, velocity=min(110, 60 + i * 3)) + for i in range(16): + hats.hit(CH, Duration.SIXTEENTH, velocity=min(80, 45 + i * 2)) + else: + hats.hit(CH, Duration.SIXTEENTH, velocity=78) + hats.hit(CH, Duration.SIXTEENTH, velocity=42) + hats.hit(CH, Duration.SIXTEENTH, velocity=60) + hats.hit(CH, Duration.SIXTEENTH, velocity=38) + hats.hit(OH, Duration.SIXTEENTH, velocity=65) + hats.hit(CH, Duration.SIXTEENTH, velocity=40) + hats.hit(CH, Duration.SIXTEENTH, velocity=58) + hats.hit(CH, Duration.SIXTEENTH, velocity=35) + hats.hit(CH, Duration.SIXTEENTH, velocity=75) + hats.hit(CH, Duration.SIXTEENTH, velocity=42) + hats.hit(CH, Duration.SIXTEENTH, velocity=55) + hats.hit(CH, Duration.SIXTEENTH, velocity=38) + hats.hit(OH, Duration.SIXTEENTH, velocity=62) + hats.hit(CH, Duration.SIXTEENTH, velocity=40) + hats.hit(CH, Duration.SIXTEENTH, velocity=58) + hats.hit(CH, Duration.SIXTEENTH, velocity=42) + snare.rest(Duration.EIGHTH) + snare.hit(S, Duration.SIXTEENTH, velocity=40) + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.QUARTER, velocity=100) + snare.rest(Duration.EIGHTH) + snare.hit(S, Duration.SIXTEENTH, velocity=42) + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.QUARTER, velocity=102) + +# Bars 57-64: fading +for vel in [88, 75, 62, 48, 35, 22, 0, 0]: + if vel > 0: + for beat in range(4): + hats.hit(CH, Duration.EIGHTH, velocity=max(15, vel - 20)) + hats.hit(CH, Duration.EIGHTH, velocity=max(12, vel - 35)) + snare.rest(Duration.QUARTER) + snare.hit(S, Duration.QUARTER, velocity=vel) + snare.rest(Duration.HALF) + else: + hats.rest(Duration.WHOLE) + snare.rest(Duration.WHOLE) + +# ── MELODIC PEAK — sine lead, bars 49-56 ───────────────────── +lead = score.part("lead", synth="sine", volume=0.4, + reverb=0.25, reverb_type="taj_mahal", + delay=0.18, delay_time=0.254, delay_feedback=0.25, + pan=-0.05) + +for _ in range(48): + lead.rest(Duration.WHOLE) + +# Bars 49-56: the melody sings above everything +# Lead melody — descending, minor, Db-C pull at the heart +lead_melody = [ + (Ab, Duration.HALF, 80), (G, Duration.QUARTER, 72), + (F, Duration.QUARTER, 70), + (Eb, Duration.HALF, 75), (Db, Duration.QUARTER, 72), + (C, Duration.QUARTER, 68), + (Db, Duration.DOTTED_HALF, 78), (C, Duration.QUARTER, 70), + (Ab.add(-12), Duration.WHOLE, 72), + (F, Duration.QUARTER, 75), (G, Duration.QUARTER, 70), + (Ab, Duration.HALF, 78), + (Bb, Duration.QUARTER, 72), (Ab, Duration.QUARTER, 70), + (G, Duration.HALF, 68), (F, Duration.HALF, 72), + (F, Duration.WHOLE, 70), + (None, Duration.WHOLE, 0), +] +for note, dur, vel in lead_melody: + if note is None: + lead.rest(dur) + else: + lead.add(note, dur, velocity=vel) + +# Bars 57-64: one last phrase, dissolving +lead.add(Ab, Duration.HALF, velocity=65) +lead.add(G, Duration.HALF, velocity=58) +lead.add(F, Duration.WHOLE, velocity=52) +for _ in range(6): + lead.rest(Duration.WHOLE) + +# ═════════════════════════════════════════════════════════════════ +import sys + +print(f"Key: {key}") +print(f"BPM: 118") +print(f"Parts: {list(score.parts.keys())}") +print(f"Duration: {score.duration_ms / 1000:.1f}s | {score.measures} measures") + +if "--live" in sys.argv: + print("Playing WAVEFORMS (live engine)...") + from pytheory_live.live import LiveEngine + engine = LiveEngine(buffer_size=1024) + engine.play_score(score) +else: + print("Playing WAVEFORMS...") + play_score(score)