From 70199fdd3decbe434e45d8cfe78e220aa8dfe765 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 30 Mar 2026 17:16:51 -0400 Subject: [PATCH] =?UTF-8?q?Track=209:=20Echo=20Chamber=20=E2=80=94=20deep?= =?UTF-8?q?=20dub,=20E=20minor,=2072=20BPM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit King Tubby at midnight. One drop kick, upright bass, offbeat skank guitar, melodica drenched in delay, trumpet stabs echoing into infinity. Cathedral reverb on everything. Space between the hits IS the music. Co-Authored-By: Claude Opus 4.6 (1M context) --- tracks/echo_chamber.py | 250 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 tracks/echo_chamber.py diff --git a/tracks/echo_chamber.py b/tracks/echo_chamber.py new file mode 100644 index 0000000..f469285 --- /dev/null +++ b/tracks/echo_chamber.py @@ -0,0 +1,250 @@ +""" +ECHO CHAMBER — deep dub. Spring reverb, delay throws, space between the hits. +King Tubby meets the studio at midnight. Every note echoes into infinity. +""" + +from pytheory import Key, Duration, Score, Tone, play_score +from pytheory.rhythm import DrumSound + +key = Key("E", "minor") +s = key.scale +prog = key.progression("i", "iv", "VII", "i") + +E = s[0]; Fs = s[1]; G = s[2]; A = s[3] +B = s[4]; C = s[5]; D = s[6] + +score = Score("4/4", bpm=72) + +K = DrumSound.KICK +S = DrumSound.SNARE +RS = DrumSound.RIMSHOT +CH = DrumSound.CLOSED_HAT +OH = DrumSound.OPEN_HAT + +# ═══════════════════════════════════════════════════════════════════ +# STRUCTURE (64 bars, ~5.5 min at 72 BPM): +# Bars 1-8: Bass + kick, sparse, establishing the weight +# Bars 9-16: Skank guitar enters, rimshots +# Bars 17-32: Melodica melody, delay throws on everything +# Bars 33-48: Peak — horns, full groove, spring reverb madness +# Bars 49-56: Breakdown — just bass and delay echoes +# Bars 57-64: Outro — everything dissolving into reverb +# ═══════════════════════════════════════════════════════════════════ + +# ── KICK + SNARE — the one drop ───────────────────────────────── +drums = score.part("drums", volume=0.4, humanize=0.1) + +# Bars 1-8: just kick on 1 and 3, sparse +for _ in range(8): + drums.hit(K, Duration.QUARTER, velocity=105) + drums.rest(Duration.QUARTER) + drums.hit(K, Duration.QUARTER, velocity=95) + drums.rest(Duration.QUARTER) + +# Bars 9-48: one drop — kick on 1, rimshot on 3 +for _ in range(40): + drums.hit(K, Duration.QUARTER, velocity=108) + drums.rest(Duration.QUARTER) + drums.hit(RS, Duration.QUARTER, velocity=88) + drums.rest(Duration.QUARTER) + +# Bars 49-56: stripped back +for _ in range(8): + drums.hit(K, Duration.QUARTER, velocity=90) + drums.rest(Duration.DOTTED_HALF) + +# Bars 57-64: fading +for bar in range(8): + vel = max(25, 85 - bar * 8) + drums.hit(K, Duration.QUARTER, velocity=vel) + drums.rest(Duration.DOTTED_HALF) + +# ── BASS — deep, round, the foundation ───────────────────────── +bass = score.part("bass", instrument="upright_bass", volume=0.45, + reverb=0.15, lowpass=600, humanize=0.1) + +# The bass line — simple, heavy, lots of space +bass_a = [ + (E.add(-24), Duration.QUARTER, 90), + (None, Duration.QUARTER, 0), + (G.add(-24), Duration.QUARTER, 82), + (E.add(-24), Duration.QUARTER, 85), +] +bass_b = [ + (A.add(-24), Duration.QUARTER, 88), + (None, Duration.QUARTER, 0), + (B.add(-24), Duration.QUARTER, 80), + (A.add(-24), Duration.QUARTER, 82), +] +bass_c = [ + (D.add(-12), Duration.QUARTER, 85), + (None, Duration.QUARTER, 0), + (C.add(-12), Duration.QUARTER, 78), + (B.add(-24), Duration.QUARTER, 80), +] +bass_d = [ + (E.add(-24), Duration.HALF, 90), + (None, Duration.HALF, 0), +] + +for _ in range(16): + for note, dur, vel in bass_a: + if note is None: + bass.rest(dur) + else: + bass.add(note, dur, velocity=vel) + for note, dur, vel in bass_b: + if note is None: + bass.rest(dur) + else: + bass.add(note, dur, velocity=vel) + for note, dur, vel in bass_c: + if note is None: + bass.rest(dur) + else: + bass.add(note, dur, velocity=vel) + for note, dur, vel in bass_d: + if note is None: + bass.rest(dur) + else: + bass.add(note, dur, velocity=vel) + +# ── SKANK GUITAR — offbeat chops, enters bar 9 ───────────────── +skank = score.part("skank", instrument="acoustic_guitar", volume=0.2, + reverb=0.3, reverb_type="cathedral", + humanize=0.12) + +for _ in range(8): + skank.rest(Duration.WHOLE) + +# Offbeat chops — the heartbeat of dub +for _ in range(10): + for chord in prog: + skank.rest(Duration.EIGHTH) + skank.add(chord, Duration.EIGHTH, velocity=62) + skank.rest(Duration.EIGHTH) + skank.add(chord, Duration.EIGHTH, velocity=58) + skank.rest(Duration.EIGHTH) + skank.add(chord, Duration.EIGHTH, velocity=60) + skank.rest(Duration.EIGHTH) + skank.add(chord, Duration.EIGHTH, velocity=55) + +# Bars 49-56: stripped +for _ in range(4): + for chord in prog: + skank.rest(Duration.QUARTER) + skank.add(chord, Duration.QUARTER, velocity=45) + skank.rest(Duration.HALF) + +# Bars 57-64: dissolving +for _ in range(2): + for chord in prog: + skank.rest(Duration.HALF) + skank.add(chord, Duration.QUARTER, velocity=35) + skank.rest(Duration.QUARTER) + +# ── MELODICA — the melody, drenched in delay, enters bar 17 ──── +melodica = score.part("melodica", instrument="harmonica", volume=0.3, + reverb=0.4, reverb_type="cathedral", + delay=0.5, delay_time=0.417, delay_feedback=0.5, + humanize=0.1) + +for _ in range(16): + melodica.rest(Duration.WHOLE) + +# Bars 17-32: simple melody — the delay does the work +mel_phrases = [ + [(B, Duration.HALF, 78), (None, Duration.HALF, 0)], + [(A, Duration.QUARTER, 72), (G, Duration.QUARTER, 68), + (E, Duration.HALF, 75)], + [(None, Duration.QUARTER, 0), (G, Duration.QUARTER, 70), + (A, Duration.QUARTER, 75), (B, Duration.QUARTER, 72)], + [(E.add(12), Duration.HALF, 80), (None, Duration.HALF, 0)], +] +for _ in range(4): + for phrase in mel_phrases: + for note, dur, vel in phrase: + if note is None: + melodica.rest(dur) + else: + melodica.add(note, dur, velocity=vel) + +# Bars 33-48: higher, more expressive +mel_high = [ + [(E.add(12), Duration.QUARTER, 82), (D.add(12), Duration.QUARTER, 78), + (B, Duration.HALF, 80)], + [(None, Duration.QUARTER, 0), (A, Duration.QUARTER, 75), + (B, Duration.HALF, 78)], + [(D.add(12), Duration.HALF, 82), (C.add(12), Duration.QUARTER, 78), + (B, Duration.QUARTER, 75)], + [(A, Duration.HALF, 78), (None, Duration.HALF, 0)], +] +for _ in range(4): + for phrase in mel_high: + for note, dur, vel in phrase: + if note is None: + melodica.rest(dur) + else: + melodica.add(note, dur, velocity=vel) + +# Bars 49-64: sparse echoes, then gone +melodica.add(B, Duration.WHOLE, velocity=65) +melodica.rest(Duration.WHOLE) +melodica.add(E, Duration.WHOLE, velocity=55) +melodica.rest(Duration.WHOLE) +for _ in range(12): + melodica.rest(Duration.WHOLE) + +# ── HORNS — trumpet stabs, delay throws, bars 33-48 ──────────── +horn = score.part("horn", instrument="trumpet", volume=0.25, + reverb=0.35, reverb_type="cathedral", + delay=0.45, delay_time=0.833, delay_feedback=0.45, + humanize=0.08) + +for _ in range(32): + horn.rest(Duration.WHOLE) + +# Bars 33-48: stabs that echo forever +horn_stabs = [ + (B, Duration.QUARTER, 85), (None, Duration.DOTTED_HALF, 0), + (None, Duration.WHOLE, 0), + (E.add(12), Duration.QUARTER, 80), (None, Duration.DOTTED_HALF, 0), + (None, Duration.WHOLE, 0), +] +for _ in range(4): + for note, dur, vel in horn_stabs: + if note is None: + horn.rest(dur) + else: + horn.add(note, dur, velocity=vel) + +# Rest of track: the delay echoes are the music +for _ in range(32): + horn.rest(Duration.WHOLE) + +# ── SPRING REVERB PAD — the space itself ──────────────────────── +spring = score.part("spring", synth="sine", envelope="pad", volume=0.1, + reverb=0.7, reverb_type="cathedral", + chorus=0.4, chorus_rate=0.1, chorus_depth=0.015, + lowpass=800) + +for _ in range(16): + for chord in prog: + spring.add(chord, Duration.WHOLE, velocity=40) + +# ═════════════════════════════════════════════════════════════════ +import sys + +print(f"Key: {key}") +print(f"BPM: 72") +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 ECHO CHAMBER (live engine)...") + from pytheory_live.live import LiveEngine + engine = LiveEngine(buffer_size=1024) + engine.play_score(score) +else: + print("Playing ECHO CHAMBER...") + play_score(score)