From 860dc1f3234e556dac51f34cecd34d026b6f2b65 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Fri, 3 Apr 2026 01:20:53 -0400 Subject: [PATCH] =?UTF-8?q?Add=20Shruti=20Lofi=20(track=2023)=20=E2=80=94?= =?UTF-8?q?=20microtonal=20lo-fi=20hip=20hop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D minor, shruti just intonation, 75 BPM. Kalimba blips, Rhodes chords, sitar hook with microtonal bends, mellotron flute pad, tambura Sa-Pa drone, lazy boom bap with ghost snares, 808 sub, vinyl crackle. Singing bowl bookends. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + README.md | 1 + play.py | 1 + tracks/shruti_lofi.py | 364 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 367 insertions(+) create mode 100644 tracks/shruti_lofi.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed7c64..0cc2043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2026-04-03 - **Track 18: Tape Memory** — Db minor, 90 BPM. Mellotron flute, FM bells, drift oscillator, crotales, granular texture, hard_sync bass, PWM lead, wavefold, ring_mod. Theremin solo at peak. Singing bowls + tingsha. Everything pytheory 0.40.9 can do. +- **Track 23: Shruti Lofi** — D minor, shruti just intonation, 75 BPM. Microtonal lo-fi hip hop. Kalimba, Rhodes, sitar, mellotron flute, tambura drone, lazy boom bap, vinyl crackle. - **Track 22: Beast Mode** — G minor, 135 BPM. Trap + sitar hook + mellotron flute drop + timpani war drums + 808 slides. Sidechained saw bass. - **Track 21: Cathedral** — D minor, 60 BPM. Tubular bells (taj_mahal 0.85), bagpipe, mellotron choir, timpani, pipe organ, mellotron strings, kick in cathedral reverb. - **Track 20: Music Box Factory** — G major, 108 BPM. Eight tuned percussion instruments stacking. Kalimba, vibraphone, celesta, marimba, glockenspiel, xylophone, crotales, timpani. Tubular bells. diff --git a/README.md b/README.md index a1f8087..09c3490 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Each track is a `.py` file. Run it to hear it. 20. **Music Box Factory** — G major, 108 BPM. Eight tuned percussion instruments only. Kalimba, vibraphone, celesta, marimba, glockenspiel, xylophone, crotales, timpani. Tubular bells mark sections. No synths, no strings — just metal and wood. 21. **Cathedral** — D minor, 60 BPM. Ancient stone. Tubular bells in taj_mahal, bagpipe drone, mellotron choir, timpani thunder, pipe organ, kick in cathedral reverb. 22. **Beast Mode** — G minor, 135 BPM. Trap drums, 808 slides, distorted saw bass, sitar hook + shred solo, mellotron flute drop, timpani war drums. The hardest track on the album. +23. **Shruti Lofi** — D minor, shruti just intonation, 75 BPM. Microtonal lo-fi hip hop. Kalimba, Rhodes, sitar hook, mellotron flute, tambura drone, lazy boom bap. Sounds like a tape found in a temple thrift store. ## Usage diff --git a/play.py b/play.py index 6514085..621e2b2 100644 --- a/play.py +++ b/play.py @@ -61,6 +61,7 @@ ALBUM_ORDER = [ "music_box_factory.py", "cathedral.py", "beast_mode.py", + "shruti_lofi.py", ] diff --git a/tracks/shruti_lofi.py b/tracks/shruti_lofi.py new file mode 100644 index 0000000..3bb89ca --- /dev/null +++ b/tracks/shruti_lofi.py @@ -0,0 +1,364 @@ +""" +SHRUTI LOFI — 22-tone microtonal lo-fi hip hop. +The pitch is slightly wrong. That's the point. +Like a tape copied so many times the intervals drifted. +D minor, shruti tuning, 75 BPM. +""" + +from pytheory import Key, Duration, Score, Tone, play_score +from pytheory.rhythm import DrumSound + +key = Key("D", "minor") +s = key.scale # 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=75, system="shruti", temperament="just") + +K = DrumSound.KICK +S = DrumSound.SNARE +CH = DrumSound.CLOSED_HAT +OH = DrumSound.OPEN_HAT + +prog = key.progression("i", "VII", "VI", "iv") + +# ═══════════════════════════════════════════════════════════════════ +# STRUCTURE (64 bars, ~3:25): +# Bars 1-8: Vinyl + kalimba — warm, wrong, beautiful +# Bars 9-16: Rhodes enters — microtonal chords +# Bars 17-24: Drums — lazy boom bap +# Bars 25-32: Sitar — the sample flip, shruti intervals +# Bars 33-40: Mellotron pad — tape on tape +# Bars 41-48: Everything together — the lo-fi dream +# Bars 49-56: Breakdown — just kalimba + 808 +# Bars 57-64: Returns soft, fades +# ═══════════════════════════════════════════════════════════════════ + +# ── VINYL — the crackle, always there ───────────────────────── +vinyl = score.part("vinyl", synth="noise", envelope="pad", volume=0.035, + lowpass=1800, highpass=500, + distortion=0.35, distortion_drive=2.5, + saturation=0.5, pan=0.1) + +for _ in range(64): + vinyl.add(D, Duration.WHOLE, velocity=22) + +# ── KALIMBA — the seed, microtonal blips ────────────────────── +kal = score.part("kalimba", instrument="kalimba", volume=0.4, + reverb=0.3, reverb_type="taj_mahal", + delay=0.15, delay_time=0.4, delay_feedback=0.25, + lowpass=3000, + pan=-0.2, humanize=0.12) + +# Bars 1-8: alone — the shruti intervals are audible, warm +kal_a = [ + (D, Duration.EIGHTH, 68), (None, Duration.EIGHTH, 0), + (F, Duration.EIGHTH, 60), (A, Duration.EIGHTH, 62), + (None, Duration.EIGHTH, 0), (F, Duration.EIGHTH, 55), + (D, Duration.EIGHTH, 65), (None, Duration.EIGHTH, 0), +] +kal_b = [ + (C, Duration.EIGHTH, 62), (None, Duration.EIGHTH, 0), + (D, Duration.EIGHTH, 58), (F, Duration.EIGHTH, 60), + (E, Duration.EIGHTH, 55), (None, Duration.EIGHTH, 0), + (D, Duration.EIGHTH, 62), (None, Duration.EIGHTH, 0), +] +for _ in range(4): + for note, dur, vel in kal_a: + if note is None: + kal.rest(dur) + else: + kal.add(note, dur, velocity=vel) + for note, dur, vel in kal_b: + if note is None: + kal.rest(dur) + else: + kal.add(note, dur, velocity=vel) + +# Bars 9-48: continues — the heartbeat +for _ in range(20): + for note, dur, vel in kal_a: + if note is None: + kal.rest(dur) + else: + kal.add(note, dur, velocity=vel) + for note, dur, vel in kal_b: + if note is None: + kal.rest(dur) + else: + kal.add(note, dur, velocity=vel) + +# Bars 49-56: breakdown — just kalimba, quieter +kal.set(volume=0.35) +for _ in range(4): + for note, dur, vel in kal_a: + if note is None: + kal.rest(dur) + else: + kal.add(note, dur, velocity=max(30, vel - 12)) + for note, dur, vel in kal_b: + if note is None: + kal.rest(dur) + else: + kal.add(note, dur, velocity=max(30, vel - 12)) + +# Bars 57-64: fading +for rep in range(4): + off = rep * -10 + for note, dur, vel in kal_a: + if note is None: + kal.rest(dur) + else: + kal.add(note, dur, velocity=max(18, vel + off - 12)) + for note, dur, vel in kal_b: + if note is None: + kal.rest(dur) + else: + kal.add(note, dur, velocity=max(18, vel + off - 12)) + +# ── RHODES — microtonal chords, enters bar 9 ────────────────── +rhodes = score.part("rhodes", instrument="electric_piano", volume=0.3, + reverb=0.4, reverb_type="taj_mahal", + delay=0.1, delay_time=0.4, delay_feedback=0.2, + tremolo_depth=0.08, tremolo_rate=2.0, + lowpass=3000, + pan=0.2, humanize=0.1) + +for _ in range(8): + rhodes.rest(Duration.WHOLE) + +# Bars 9-48: sparse chords — the shruti tuning makes them shimmer +for section in range(10): + p = prog if section % 2 == 0 else key.progression("i", "v", "VI", "iv") + for chord in p: + rhodes.add(chord, Duration.EIGHTH, velocity=58) + rhodes.rest(Duration.DOTTED_QUARTER) + rhodes.rest(Duration.HALF) + +# Bars 49-56: silent +for _ in range(8): + rhodes.rest(Duration.WHOLE) + +# Bars 57-64: returns fading +for vel in [48, 42, 35, 28]: + for chord in prog: + rhodes.add(chord, Duration.EIGHTH, velocity=vel) + rhodes.rest(Duration.DOTTED_QUARTER) + rhodes.rest(Duration.HALF) + +# ── 808 — warm, round, enters bar 9 ────────────────────────── +sub = score.part("808", synth="sine", envelope="pad", volume=0.5, + lowpass=180, distortion=0.12, distortion_drive=2.0, + sub_osc=0.4, sidechain=0.3) + +for _ in range(8): + sub.rest(Duration.WHOLE) + +# Bars 9-48: continuous wave, follows roots +roots = [D.add(-24), C.add(-24), Bb.add(-24), G.add(-24)] +for _ in range(10): + for root in roots: + sub.add(root, Duration.WHOLE, velocity=35) + +# Bars 49-56: just the root — breakdown +for _ in range(8): + sub.add(D.add(-24), Duration.WHOLE, velocity=30) + +# Bars 57-64: fading +for vel in [28, 25, 22, 18, 14, 10, 5, 0]: + if vel > 0: + sub.add(D.add(-24), Duration.WHOLE, velocity=vel) + else: + sub.rest(Duration.WHOLE) + +# ── DRUMS — lazy boom bap, enters bar 17 ────────────────────── +kick = score.part("kick", volume=0.55, humanize=0.06, + reverb=0.1, lowpass=5000) +snare = score.part("snare", volume=0.35, humanize=0.06, + reverb=0.2, reverb_decay=1.0, + delay=0.06, delay_time=0.4, delay_feedback=0.1, + pan=0.05) +hats = score.part("hats", volume=0.2, pan=0.15, humanize=0.06) + +for _ in range(16): + kick.rest(Duration.WHOLE) + snare.rest(Duration.WHOLE) + hats.rest(Duration.WHOLE) + +# Bars 17-48: lo-fi beat — lazy, behind the grid +for _ in range(32): + # Kick on 1 and and-of-2 + kick.hit(K, Duration.QUARTER, velocity=92) + kick.rest(Duration.EIGHTH) + kick.hit(K, Duration.EIGHTH, velocity=75) + kick.rest(Duration.QUARTER) + kick.rest(Duration.QUARTER) + + # Snare on 2 and 4 with ghost before + snare.rest(Duration.EIGHTH) + snare.hit(S, Duration.SIXTEENTH, velocity=35) + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.QUARTER, velocity=85) + snare.rest(Duration.EIGHTH) + snare.hit(S, Duration.SIXTEENTH, velocity=32) + snare.rest(Duration.SIXTEENTH) + snare.hit(S, Duration.QUARTER, velocity=88) + + # Hats — lazy 8ths, some louder + hats.hit(CH, Duration.EIGHTH, velocity=58) + hats.hit(CH, Duration.EIGHTH, velocity=35) + hats.hit(CH, Duration.EIGHTH, velocity=52) + hats.hit(CH, Duration.EIGHTH, velocity=38) + hats.hit(CH, Duration.EIGHTH, velocity=55) + hats.hit(CH, Duration.EIGHTH, velocity=32) + hats.hit(OH, Duration.EIGHTH, velocity=48) + hats.hit(CH, Duration.EIGHTH, velocity=35) + +# Bars 49-56: breakdown — half time +for _ in range(8): + kick.hit(K, Duration.HALF, velocity=82) + kick.rest(Duration.HALF) + snare.rest(Duration.HALF) + snare.hit(S, Duration.HALF, velocity=72) + hats.rest(Duration.WHOLE) + +# Bars 57-64: returns fading +for vel in [80, 72, 62, 52, 40, 28, 0, 0]: + if vel > 0: + kick.hit(K, Duration.QUARTER, velocity=vel) + kick.rest(Duration.DOTTED_HALF) + snare.rest(Duration.QUARTER) + snare.hit(S, Duration.QUARTER, velocity=max(15, vel - 8)) + snare.rest(Duration.HALF) + hats.hit(CH, Duration.QUARTER, velocity=max(15, vel - 25)) + hats.rest(Duration.DOTTED_HALF) + else: + kick.rest(Duration.WHOLE) + snare.rest(Duration.WHOLE) + hats.rest(Duration.WHOLE) + +# ── SITAR — the sample flip, enters bar 25 ──────────────────── +sitar = score.part("sitar", instrument="sitar", volume=0.4, + reverb=0.2, reverb_type="taj_mahal", + delay=0.12, delay_time=0.4, delay_feedback=0.2, + lowpass=3500, + pan=-0.25, humanize=0.1) + +for _ in range(24): + sitar.rest(Duration.WHOLE) + +# Bars 25-32: the hook — shruti intervals make it haunting +sitar_hook = [ + (D, Duration.QUARTER, 72, -0.1), (F, Duration.EIGHTH, 65, 0.0), + (E, Duration.EIGHTH, 62, 0.0), (D, Duration.HALF, 68, -0.08), + (None, Duration.QUARTER, 0, 0.0), (A.add(-12), Duration.QUARTER, 60, 0.1), + (D, Duration.QUARTER, 68, 0.0), (F, Duration.QUARTER, 65, -0.1), + (G, Duration.HALF, 70, 0.0), (F, Duration.HALF, 65, -0.08), +] +for _ in range(4): + for note, dur, vel, bend in sitar_hook: + if note is None: + sitar.rest(dur) + else: + sitar.add(note, dur, velocity=vel, bend=bend) + +# Bars 33-48: continues under mellotron +for _ in range(8): + for note, dur, vel, bend in sitar_hook: + if note is None: + sitar.rest(dur) + else: + sitar.add(note, dur, velocity=max(35, vel - 8), bend=bend) + +# Bars 49-56: one note — breathing +sitar.add(D, Duration.WHOLE, velocity=55, bend=-0.1) +sitar.rest(Duration.WHOLE) +sitar.add(A.add(-12), Duration.WHOLE, velocity=48, bend=0.08) +sitar.rest(Duration.WHOLE) +sitar.rest(Duration.WHOLE) +sitar.rest(Duration.WHOLE) +sitar.rest(Duration.WHOLE) +sitar.rest(Duration.WHOLE) + +# Bars 57-64: hook returns fading +for vel in [58, 50, 42, 35]: + for note, dur, v, bend in sitar_hook: + if note is None: + sitar.rest(dur) + else: + sitar.add(note, dur, velocity=max(20, vel - 10), bend=bend) + +# ── MELLOTRON FLUTE — tape pad, enters bar 33 ───────────────── +mello = score.part("mellotron", instrument="mellotron_flute", volume=0.2, + reverb=0.35, reverb_type="taj_mahal", + lowpass=2500, + pan=0.15, humanize=0.08) + +for _ in range(32): + mello.rest(Duration.WHOLE) + +# Bars 33-48: warm chords — tape warble + shruti = magic +for _ in range(4): + for chord in prog: + mello.add(chord, Duration.WHOLE, velocity=48) + +# Bars 49-56: silent +for _ in range(8): + mello.rest(Duration.WHOLE) + +# Bars 57-64: returns fading +for vel in [42, 35, 28, 22, 15, 10, 0, 0]: + if vel > 0: + mello.add(prog[0], Duration.WHOLE, velocity=vel) + else: + mello.rest(Duration.WHOLE) + +# ── TAMBURA — the shruti drone, enters bar 9 ────────────────── +tambura = score.part("tambura", synth="sine", envelope="pad", volume=0.1, + reverb=0.35, reverb_type="taj_mahal", + chorus=0.3, chorus_rate=0.05, chorus_depth=0.01, + lowpass=800, pan=-0.1) + +for _ in range(8): + tambura.rest(Duration.WHOLE) + +# Sa-Pa drone — in shruti tuning the fifth is pure, not tempered +for _ in range(48): + tambura.add(D.add(-24), Duration.HALF, velocity=38) + tambura.add(A.add(-24), Duration.HALF, velocity=32) + +for vel in [30, 25, 20, 15, 10, 5, 0, 0]: + if vel > 0: + tambura.add(D.add(-24), Duration.WHOLE, velocity=vel) + else: + tambura.rest(Duration.WHOLE) + +# ── SINGING BOWL — just two strikes ─────────────────────────── +bowl = score.part("bowl", instrument="singing_bowl", volume=0.25, + reverb=0.5, reverb_type="taj_mahal", + delay=0.12, delay_time=0.8, delay_feedback=0.15, + pan=0.2) + +bowl.add(D.add(-24), Duration.WHOLE, velocity=55) +for _ in range(62): + bowl.rest(Duration.WHOLE) +bowl.add(D.add(-24), Duration.WHOLE, velocity=42) + +# ═════════════════════════════════════════════════════════════════ +import sys + +print(f"Key: {key}") +print(f"System: shruti / just intonation") +print(f"BPM: 75") +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 SHRUTI LOFI (live engine)...") + from pytheory_live.live import LiveEngine + engine = LiveEngine(buffer_size=1024) + engine.play_score(score) +else: + print("Playing SHRUTI LOFI...") + play_score(score)