diff --git a/tracks/2am.py b/tracks/2am.py new file mode 100644 index 0000000..aebed06 --- /dev/null +++ b/tracks/2am.py @@ -0,0 +1,216 @@ +""" +2AM — lo-fi hip hop study beats. Late night, rain on the window. +Rhodes, vinyl crackle, lazy drums, upright bass, saxophone. +Just vibes. +""" + +from pytheory import Key, Duration, Score, Tone, play_score +from pytheory.rhythm import DrumSound + +key = Key("Eb", "minor") +s = key.scale +prog = key.progression("i", "VI", "iv", "VII") + +# Eb minor: Eb F Gb Ab Bb Cb Db +Eb = s[0]; F = s[1]; Gb = s[2]; Ab = s[3] +Bb = s[4]; Cb = s[5]; Db = s[6] + +score = Score("4/4", bpm=72, swing=0.15) + +K = DrumSound.KICK +S = DrumSound.SNARE +CH = DrumSound.CLOSED_HAT + +# ═══════════════════════════════════════════════════════════════════ +# STRUCTURE (64 bars, ~5.5 min at 72 BPM): +# Bars 1-8: Rhodes alone, establishing the vibe +# Bars 9-16: Drums enter, lazy boom-bap +# Bars 17-32: Bass enters, the groove settles +# Bars 33-48: Sax melody — the heart +# Bars 49-56: Sax + rhodes trading, peak warmth +# Bars 57-64: Stripping back, rhodes alone again +# ═══════════════════════════════════════════════════════════════════ + +# ── RHODES — the soul of lo-fi ────────────────────────────────── +rhodes = score.part("rhodes", instrument="electric_piano", volume=0.35, + reverb=0.5, reverb_type="taj_mahal", + tremolo_depth=0.12, tremolo_rate=2.5, + chorus=0.2, chorus_rate=0.3, chorus_depth=0.005, + humanize=0.12, saturation=0.15) + +# Full 64 bars — chords throughout, the anchor +for _ in range(16): + for chord in prog: + rhodes.add(chord, Duration.WHOLE, velocity=65) + +# ── VINYL CRACKLE — noise texture, always on ─────────────────── +vinyl = score.part("vinyl", synth="noise", envelope="pad", volume=0.04, + lowpass=3000) + +for _ in range(64): + vinyl.add(Eb, Duration.WHOLE, velocity=40) + +# ── DRUMS — lazy boom-bap, enters bar 9 ──────────────────────── +drums = score.part("drums", volume=0.35, humanize=0.12) + +for _ in range(8): + drums.rest(Duration.WHOLE) + +# Bars 9-56: the laziest beat alive +for bar in range(48): + # Kick on 1, ghost kick on the & of 2 + drums.hit(K, Duration.QUARTER, velocity=90) + drums.hit(CH, Duration.EIGHTH, velocity=45) + drums.hit(K, Duration.EIGHTH, velocity=55) + # Snare on 2 + drums.hit(S, Duration.QUARTER, velocity=82) + # Kick ghost on & of 3 + drums.hit(K, Duration.EIGHTH, velocity=50) + drums.hit(CH, Duration.EIGHTH, velocity=42) + drums.hit(S, Duration.QUARTER, velocity=78) + +# Bars 57-64: fading out +for bar in range(8): + vel = max(25, 85 - bar * 7) + drums.hit(K, Duration.QUARTER, velocity=vel) + drums.hit(CH, Duration.EIGHTH, velocity=max(15, vel - 40)) + drums.hit(K, Duration.EIGHTH, velocity=max(15, vel - 35)) + drums.hit(S, Duration.QUARTER, velocity=max(20, vel - 8)) + drums.hit(K, Duration.EIGHTH, velocity=max(15, vel - 38)) + drums.hit(CH, Duration.EIGHTH, velocity=max(15, vel - 42)) + drums.hit(S, Duration.QUARTER, velocity=max(20, vel - 10)) + +# ── UPRIGHT BASS — warm, round, enters bar 17 ────────────────── +bass = score.part("bass", instrument="upright_bass", volume=0.35, + reverb=0.2, lowpass=800, humanize=0.1) + +for _ in range(16): + bass.rest(Duration.WHOLE) + +# Bars 17-56: walking bass following the roots +bass_lines = [ + # i — Ebm + [(Eb.add(-24), Duration.QUARTER, 85), + (Gb.add(-24), Duration.QUARTER, 72), + (Bb.add(-24), Duration.QUARTER, 78), + (Ab.add(-24), Duration.QUARTER, 70)], + # VI — Cb + [(Cb.add(-24), Duration.QUARTER, 82), + (Eb.add(-12), Duration.QUARTER, 70), + (Db.add(-24), Duration.QUARTER, 75), + (Cb.add(-24), Duration.QUARTER, 68)], + # iv — Abm + [(Ab.add(-24), Duration.QUARTER, 85), + (Cb.add(-24), Duration.QUARTER, 72), + (Bb.add(-24), Duration.QUARTER, 78), + (Ab.add(-24), Duration.QUARTER, 70)], + # VII — Db + [(Db.add(-24), Duration.QUARTER, 82), + (F.add(-24), Duration.QUARTER, 70), + (Ab.add(-24), Duration.QUARTER, 75), + (Db.add(-12), Duration.QUARTER, 68)], +] +for _ in range(10): + for bass_bar in bass_lines: + for note, dur, vel in bass_bar: + bass.add(note, dur, velocity=vel) + +# Bars 57-64: fading +for _ in range(2): + for bass_bar in bass_lines: + for note, dur, vel in bass_bar: + bass.add(note, dur, velocity=max(30, vel - 25)) + +# ── SAXOPHONE — the melody, enters bar 33 ────────────────────── +sax = score.part("sax", instrument="saxophone", volume=0.35, + reverb=0.45, reverb_type="taj_mahal", + delay=0.15, delay_time=0.417, delay_feedback=0.25, + humanize=0.1) + +for _ in range(32): + sax.rest(Duration.WHOLE) + +# Bars 33-48: lazy, bluesy melody — lots of space +sax_phrases = [ + # Phrase 1: opening statement + [(Bb, Duration.HALF, 82), (Ab, Duration.QUARTER, 75), + (Gb, Duration.QUARTER, 70)], + [(Eb, Duration.HALF, 78), (None, Duration.HALF, 0)], + # Phrase 2: climbing + [(None, Duration.QUARTER, 0), (Gb, Duration.QUARTER, 72), + (Ab, Duration.QUARTER, 78), (Bb, Duration.QUARTER, 82)], + [(Db.add(12), Duration.HALF, 85), (Cb, Duration.QUARTER, 75), + (Bb, Duration.QUARTER, 72)], + # Phrase 3: the cry + [(Bb, Duration.DOTTED_HALF, 88), + (Ab, Duration.QUARTER, 78)], + [(Gb, Duration.QUARTER, 72), (Eb, Duration.QUARTER, 68), + (None, Duration.HALF, 0)], + # Phrase 4: resolution + [(Eb, Duration.QUARTER, 75), (F, Duration.QUARTER, 72), + (Gb, Duration.QUARTER, 78), (Ab, Duration.QUARTER, 75)], + [(Eb, Duration.WHOLE, 80)], +] +for _ in range(2): + for phrase in sax_phrases: + for note, dur, vel in phrase: + if note is None: + sax.rest(dur) + else: + sax.add(note, dur, velocity=vel) + +# Bars 49-56: higher, more expressive +sax_high = [ + [(Eb.add(12), Duration.HALF, 90), (Db.add(12), Duration.QUARTER, 82), + (Cb, Duration.QUARTER, 78)], + [(Bb, Duration.HALF, 85), (None, Duration.QUARTER, 0), + (Ab, Duration.QUARTER, 75)], + [(Bb, Duration.QUARTER, 80), (Db.add(12), Duration.QUARTER, 85), + (Eb.add(12), Duration.HALF, 92)], + [(Db.add(12), Duration.QUARTER, 82), (Bb, Duration.QUARTER, 78), + (Ab, Duration.HALF, 75)], +] +for _ in range(2): + for phrase in sax_high: + for note, dur, vel in phrase: + if note is None: + sax.rest(dur) + else: + sax.add(note, dur, velocity=vel) + +# Bars 57-64: one last phrase, fading +for phrase in sax_phrases[:4]: + for note, dur, vel in phrase: + if note is None: + sax.rest(dur) + else: + sax.add(note, dur, velocity=max(35, vel - 20)) +for _ in range(4): + sax.rest(Duration.WHOLE) + +# ── PAD — warmth underneath ───────────────────────────────────── +pad = score.part("pad", synth="supersaw", envelope="pad", volume=0.08, + reverb=0.6, reverb_type="taj_mahal", + chorus=0.3, chorus_rate=0.15, chorus_depth=0.008, + lowpass=1200) + +for _ in range(16): + for chord in prog: + pad.add(chord, Duration.WHOLE, velocity=45) + +# ═════════════════════════════════════════════════════════════════ +import sys + +print(f"Key: {key}") +print(f"BPM: 72 (swing 0.15)") +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 2AM (live engine)...") + from pytheory_live.live import LiveEngine + engine = LiveEngine(buffer_size=1024) + engine.play_score(score) +else: + print("Playing 2AM...") + play_score(score) diff --git a/tracks/silk_road.py b/tracks/silk_road.py new file mode 100644 index 0000000..5fd1664 --- /dev/null +++ b/tracks/silk_road.py @@ -0,0 +1,322 @@ +""" +SILK ROAD — a caravan picking up musicians along the ancient trade route. +Each civilization layers on top, nobody leaves. D minor throughout. + +China → India → Persia → Mediterranean → All Together +""" + +from pytheory import Key, Duration, Score, Tone, play_score +from pytheory.rhythm import DrumSound + +key = Key("D", "minor") +s = key.scale + +# D minor: D E F G A Bb C +D = s[0]; E = s[1]; F = s[2]; G = s[3] +A = s[4]; Bb = s[5]; C = s[6] + +score = Score("4/4", bpm=95) + +# ═══════════════════════════════════════════════════════════════════ +# STRUCTURE (80 bars, ~5 min): +# Bars 1-16: China — koto solo, pentatonic +# Bars 17-32: India — sitar + tabla join the koto +# Bars 33-48: Persia — mandolin (oud) + doumbek layer in +# Bars 49-64: Mediterranean — guitar + cajon, flamenco tinge +# Bars 65-80: All Together — full caravan, everyone plays +# ═══════════════════════════════════════════════════════════════════ + +# ── KOTO — China, pentatonic, sparse ─────────────────────────── +# D minor pentatonic: D F G A C (removes E and Bb) +koto = score.part("koto", instrument="koto", volume=0.4, + reverb=0.5, reverb_type="taj_mahal", + delay=0.2, delay_time=0.316, delay_feedback=0.3, + humanize=0.1) + +# Bars 1-16: solo koto — contemplative, lots of space +koto_phrases = [ + # Phrase 1: simple, announcing + [(D.add(12), Duration.HALF, 80), (None, Duration.QUARTER, 0), + (A, Duration.QUARTER, 70)], + [(G, Duration.QUARTER, 75), (F, Duration.QUARTER, 68), + (D, Duration.HALF, 72)], + # Phrase 2: climbing + [(None, Duration.QUARTER, 0), (A, Duration.QUARTER, 75), + (C.add(12), Duration.QUARTER, 82), (D.add(12), Duration.QUARTER, 78)], + [(F.add(12), Duration.HALF, 85), (D.add(12), Duration.QUARTER, 75), + (None, Duration.QUARTER, 0)], + # Phrase 3: descending + [(D.add(12), Duration.QUARTER, 80), (C.add(12), Duration.EIGHTH, 72), + (A, Duration.EIGHTH, 68), (G, Duration.HALF, 75)], + [(F, Duration.QUARTER, 70), (D, Duration.QUARTER, 65), + (None, Duration.HALF, 0)], + # Phrase 4: resolving + [(A, Duration.QUARTER, 78), (G, Duration.EIGHTH, 70), + (F, Duration.EIGHTH, 65), (G, Duration.QUARTER, 72), + (A, Duration.QUARTER, 75)], + [(D, Duration.WHOLE, 80)], +] +for _ in range(2): + for phrase in koto_phrases: + for note, dur, vel in phrase: + if note is None: + koto.rest(dur) + else: + koto.add(note, dur, velocity=vel) + +# Bars 17-80: koto continues, same phrases, quieter as others join +for _ in range(2): + for phrase in koto_phrases: + for note, dur, vel in phrase: + if note is None: + koto.rest(dur) + else: + koto.add(note, dur, velocity=max(35, vel - 15)) + +for _ in range(6): + for phrase in koto_phrases: + for note, dur, vel in phrase: + if note is None: + koto.rest(dur) + else: + koto.add(note, dur, velocity=max(30, vel - 25)) + +# ── SITAR — India, enters bar 17 ─────────────────────────────── +sitar = score.part("sitar", instrument="sitar", volume=0.45, + reverb=0.35, reverb_type="taj_mahal", + delay=0.25, delay_time=0.333, delay_feedback=0.3, + pan=-0.2, humanize=0.1) + +# Bars 1-16: silent +for _ in range(16): + sitar.rest(Duration.WHOLE) + +# Bars 17-32: sitar melody — raga-influenced, ornamental +sitar_phrases = [ + [(D, Duration.HALF, 85), (E, Duration.QUARTER, 75), + (F, Duration.QUARTER, 80)], + [(G, Duration.QUARTER, 85), (A, Duration.EIGHTH, 78), + (G, Duration.EIGHTH, 72), (F, Duration.HALF, 80)], + [(A, Duration.QUARTER, 90), (Bb, Duration.EIGHTH, 82), + (A, Duration.EIGHTH, 78), (G, Duration.QUARTER, 75), + (F, Duration.QUARTER, 70)], + [(E, Duration.QUARTER, 72), (D, Duration.DOTTED_HALF, 85)], +] +for _ in range(4): + for phrase in sitar_phrases: + for note, dur, vel in phrase: + sitar.add(note, dur, velocity=vel) + +# Bars 33-80: sitar continues through all sections +for _ in range(12): + for phrase in sitar_phrases: + for note, dur, vel in phrase: + sitar.add(note, dur, velocity=vel) + +# ── TABLA — India, enters bar 17 ─────────────────────────────── +NA = DrumSound.TABLA_NA +TIT = DrumSound.TABLA_TIT +GE = DrumSound.TABLA_GE +tDHA = DrumSound.TABLA_DHA +GEB = DrumSound.TABLA_GE_BEND + +tabla = score.part("tabla", volume=0.3, reverb=0.2, reverb_decay=1.0, + humanize=0.08) + +for _ in range(16): + tabla.rest(Duration.WHOLE) + +# Bars 17-80: keherwa groove throughout +for bar in range(64): + if bar % 8 == 7: + tabla.hit(tDHA, Duration.EIGHTH, velocity=95, articulation="accent") + tabla.hit(GEB, Duration.EIGHTH, velocity=108) + tabla.hit(NA, Duration.EIGHTH, velocity=72) + tabla.hit(GEB, Duration.EIGHTH, velocity=105) + tabla.hit(tDHA, Duration.EIGHTH, velocity=90, articulation="accent") + tabla.hit(NA, Duration.SIXTEENTH, velocity=62) + tabla.hit(TIT, Duration.SIXTEENTH, velocity=52) + tabla.hit(GEB, Duration.QUARTER, velocity=112) + else: + tabla.hit(tDHA, Duration.EIGHTH, velocity=85, articulation="accent") + tabla.hit(GE, Duration.EIGHTH, velocity=55) + tabla.hit(NA, Duration.EIGHTH, velocity=65) + tabla.hit(TIT, Duration.EIGHTH, velocity=42) + tabla.hit(NA, Duration.EIGHTH, velocity=60) + tabla.hit(TIT, Duration.EIGHTH, velocity=38) + tabla.hit(tDHA, Duration.EIGHTH, velocity=80, articulation="accent") + tabla.hit(NA, Duration.EIGHTH, velocity=58) + +# ── MANDOLIN (oud) — Persia, enters bar 33 ───────────────────── +oud = score.part("oud", instrument="mandolin", volume=0.4, + reverb=0.4, reverb_type="cathedral", + delay=0.15, delay_time=0.25, delay_feedback=0.25, + pan=0.2, humanize=0.08) + +for _ in range(32): + oud.rest(Duration.WHOLE) + +# Bars 33-80: maqam-influenced melody — chromatic ornaments +oud_phrases = [ + # Descending maqam feel + [(A, Duration.EIGHTH, 88), (Bb, Duration.EIGHTH, 78), + (A, Duration.QUARTER, 85), (G, Duration.QUARTER, 80), + (F, Duration.QUARTER, 75)], + [(E, Duration.EIGHTH, 72), (F, Duration.EIGHTH, 68), + (E, Duration.QUARTER, 75), (D, Duration.HALF, 82)], + # Climbing with ornaments + [(D, Duration.EIGHTH, 80), (E, Duration.SIXTEENTH, 70), + (F, Duration.SIXTEENTH, 72), (G, Duration.QUARTER, 82), + (A, Duration.QUARTER, 88), (None, Duration.QUARTER, 0)], + [(Bb, Duration.QUARTER, 85), (A, Duration.EIGHTH, 78), + (G, Duration.EIGHTH, 72), (A, Duration.HALF, 80)], +] +for _ in range(12): + for phrase in oud_phrases: + for note, dur, vel in phrase: + if note is None: + oud.rest(dur) + else: + oud.add(note, dur, velocity=vel) + +# ── DOUMBEK — Persia, enters bar 33 ──────────────────────────── +DKD = DrumSound.DOUMBEK_DUM +DKT = DrumSound.DOUMBEK_TEK +DKK = DrumSound.DOUMBEK_KA + +doumbek = score.part("doumbek", volume=0.3, reverb=0.15, humanize=0.06) + +for _ in range(32): + doumbek.rest(Duration.WHOLE) + +# Bars 33-80: maqsoum-inspired rhythm +for bar in range(48): + if bar % 8 == 7: + # Fill + doumbek.hit(DKD, Duration.SIXTEENTH, velocity=100, articulation="accent") + doumbek.hit(DKT, Duration.SIXTEENTH, velocity=65) + doumbek.hit(DKT, Duration.SIXTEENTH, velocity=62) + doumbek.hit(DKD, Duration.SIXTEENTH, velocity=95) + doumbek.hit(DKT, Duration.SIXTEENTH, velocity=68) + doumbek.hit(DKK, Duration.SIXTEENTH, velocity=58) + doumbek.hit(DKD, Duration.SIXTEENTH, velocity=100, articulation="accent") + doumbek.hit(DKT, Duration.SIXTEENTH, velocity=65) + doumbek.hit(DKT, Duration.SIXTEENTH, velocity=60) + doumbek.hit(DKK, Duration.SIXTEENTH, velocity=55) + doumbek.hit(DKD, Duration.SIXTEENTH, velocity=98) + doumbek.hit(DKT, Duration.SIXTEENTH, velocity=62) + doumbek.hit(DKD, Duration.QUARTER, velocity=110, articulation="marcato") + else: + # Maqsoum: DUM-tek-tek-DUM-tek + doumbek.hit(DKD, Duration.EIGHTH, velocity=90) + doumbek.hit(DKT, Duration.EIGHTH, velocity=60) + doumbek.hit(DKT, Duration.EIGHTH, velocity=58) + doumbek.hit(DKD, Duration.EIGHTH, velocity=85) + doumbek.hit(DKT, Duration.EIGHTH, velocity=62) + doumbek.hit(DKT, Duration.EIGHTH, velocity=55) + doumbek.hit(DKT, Duration.EIGHTH, velocity=58) + doumbek.hit(DKT, Duration.EIGHTH, velocity=52) + +# ── GUITAR — Mediterranean, enters bar 49 ────────────────────── +guitar = score.part("guitar", instrument="acoustic_guitar", volume=0.35, + reverb=0.35, reverb_type="cathedral", + humanize=0.1) + +for _ in range(48): + guitar.rest(Duration.WHOLE) + +# Bars 49-80: fingerpicked arpeggios — flamenco-tinged +prog = key.progression("i", "VII", "VI", "v") +guitar_arps = [ + # i — Dm + [D.add(-12), A.add(-12), D, F, A, F, D, A.add(-12)], + # VII — C + [C.add(-12), G.add(-12), C, E, G, E, C, G.add(-12)], + # VI — Bb + [Bb.add(-24), F.add(-12), Bb.add(-12), D, F, D, Bb.add(-12), F.add(-12)], + # v — Am + [A.add(-24), E.add(-12), A.add(-12), C, E, C, A.add(-12), E.add(-12)], +] +for _ in range(8): + for arp in guitar_arps: + for note in arp: + guitar.add(note, Duration.EIGHTH, velocity=75) + +# ── CAJON — Mediterranean, enters bar 49 ──────────────────────── +CS = DrumSound.CAJON_SLAP +CT = DrumSound.CAJON_TAP +CSS = DrumSound.CAJON_SLAP_SNARE + +cajon = score.part("cajon", volume=0.25, reverb=0.3, reverb_type="cathedral", + humanize=0.08) + +for _ in range(48): + cajon.rest(Duration.WHOLE) + +# Bars 49-80: flamenco-ish groove +for bar in range(32): + if bar % 4 == 3: + # Fill + cajon.hit(CSS, Duration.SIXTEENTH, velocity=100, articulation="accent") + cajon.hit(CT, Duration.SIXTEENTH, velocity=55) + cajon.hit(CSS, Duration.SIXTEENTH, velocity=98) + cajon.hit(CT, Duration.SIXTEENTH, velocity=52) + cajon.hit(CSS, Duration.SIXTEENTH, velocity=102, articulation="accent") + cajon.hit(CT, Duration.SIXTEENTH, velocity=55) + cajon.hit(CS, Duration.SIXTEENTH, velocity=95) + cajon.hit(CT, Duration.SIXTEENTH, velocity=50) + cajon.hit(CSS, Duration.SIXTEENTH, velocity=105, articulation="accent") + cajon.hit(CT, Duration.SIXTEENTH, velocity=55) + cajon.hit(CS, Duration.SIXTEENTH, velocity=92) + cajon.hit(CT, Duration.SIXTEENTH, velocity=52) + cajon.hit(CSS, Duration.QUARTER, velocity=110, articulation="marcato") + else: + cajon.hit(CS, Duration.EIGHTH, velocity=88, articulation="accent") + cajon.hit(CT, Duration.EIGHTH, velocity=48) + cajon.hit(CSS, Duration.EIGHTH, velocity=82) + cajon.hit(CT, Duration.EIGHTH, velocity=45) + cajon.hit(CS, Duration.EIGHTH, velocity=85, articulation="accent") + cajon.hit(CT, Duration.EIGHTH, velocity=50) + cajon.hit(CSS, Duration.EIGHTH, velocity=78) + cajon.hit(CT, Duration.EIGHTH, velocity=45) + +# ── TAMBURA — the thread connecting everything ────────────────── +tambura = score.part("tambura", synth="sine", envelope="pad", volume=0.15, + reverb=0.7, reverb_type="taj_mahal", + chorus=0.3, chorus_rate=0.08, chorus_depth=0.01, + lowpass=1000) + +for _ in range(80): + tambura.add(D.add(-24), Duration.HALF) + tambura.add(A.add(-24), Duration.HALF) + +# ── PAD — swells in the last section ─────────────────────────── +pad = score.part("pad", synth="supersaw", envelope="pad", volume=0.1, + reverb=0.6, reverb_type="taj_mahal", + chorus=0.4, chorus_rate=0.2, chorus_depth=0.008, + lowpass=1800) + +for _ in range(64): + pad.rest(Duration.WHOLE) + +# Bars 65-80: all together, pad glues it +for _ in range(4): + for chord in prog: + pad.add(chord, Duration.WHOLE, velocity=60) + +# ═════════════════════════════════════════════════════════════════ +import sys + +print(f"Key: {key}") +print(f"BPM: 95") +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 SILK ROAD (live engine)...") + from pytheory_live.live import LiveEngine + engine = LiveEngine(buffer_size=1024) + engine.play_score(score) +else: + print("Playing SILK ROAD...") + play_score(score)