mirror of
https://github.com/kennethreitz/interpretations.git
synced 2026-06-18 13:50:59 +00:00
00fc35de19
Every track: tuple-unpacked scale degrees, small local helpers (rest_bars, chord_bars, play_phrase), data-driven drum patterns and phrase tuples, sparse-event dicts, explicit velocity lists for fades, dead code removed. Net -1,415 lines across 25 files. Adds .fingerprint.py, a verification harness that hashes every audible parameter of a score (notes, voicings, velocities, bends, drum hits, LFO automation, part settings). All 25 tracks fingerprint identical to their pre-refactor baselines, stored in .fingerprints/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
310 lines
10 KiB
Python
310 lines
10 KiB
Python
"""
|
|
CATHEDRAL — ancient stone, heavy air.
|
|
Bagpipe drone, timpani thunder, mellotron choir, tubular bells.
|
|
The sound of a building that has stood for a thousand years.
|
|
D minor, 60 BPM.
|
|
"""
|
|
|
|
from pytheory import Key, Duration, Score, play_score
|
|
from pytheory.rhythm import DrumSound
|
|
|
|
key = Key("D", "minor")
|
|
s = key.scale # D E F G A Bb C
|
|
|
|
D, E, F, G, A, Bb, C = s[:7]
|
|
|
|
score = Score("4/4", bpm=60)
|
|
|
|
prog = key.progression("i", "iv", "VII", "i")
|
|
prog2 = key.progression("i", "VI", "iv", "V")
|
|
|
|
|
|
def rest_bars(part, n):
|
|
for _ in range(n):
|
|
part.rest(Duration.WHOLE)
|
|
|
|
|
|
def chord_bars(part, chords, vel, repeats):
|
|
for _ in range(repeats):
|
|
for chord in chords:
|
|
part.add(chord, Duration.WHOLE, velocity=vel)
|
|
|
|
|
|
def fade_bars(part, note, vels):
|
|
for vel in vels:
|
|
if vel > 0:
|
|
part.add(note, Duration.WHOLE, velocity=vel)
|
|
else:
|
|
part.rest(Duration.WHOLE)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# STRUCTURE (64 bars, ~4:16):
|
|
# Bars 1-8: Tubular bells alone — echoing in empty stone
|
|
# Bars 9-16: Bagpipe drone — the ancient breath
|
|
# Bars 17-24: Mellotron choir enters — voices from the walls
|
|
# Bars 25-32: Timpani — the heartbeat of the building
|
|
# Bars 33-40: Pipe organ — the full weight of God
|
|
# Bars 41-48: All together — the cathedral sings
|
|
# Bars 49-56: Mellotron choir solo — the most human moment
|
|
# Bars 57-64: Bells alone again — the echo outlasts us all
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
# ── TUBULAR BELLS — the space itself ──────────────────────────
|
|
bells = score.part("bells", instrument="tubular_bells", volume=0.4,
|
|
reverb=0.85, reverb_type="taj_mahal",
|
|
delay=0.3, delay_time=1.0, delay_feedback=0.35,
|
|
pan=0.15)
|
|
|
|
|
|
def bell_tolls(v1, v2, v3):
|
|
bells.add(D.add(-12), Duration.WHOLE, velocity=v1)
|
|
rest_bars(bells, 2)
|
|
bells.add(A.add(-12), Duration.WHOLE, velocity=v2)
|
|
rest_bars(bells, 2)
|
|
bells.add(D.add(-12), Duration.WHOLE, velocity=v3)
|
|
bells.rest(Duration.WHOLE)
|
|
|
|
|
|
# Bars 1-8: alone — one strike, let it ring, another
|
|
bell_tolls(65, 58, 62)
|
|
|
|
# Bars 9-56: every 4 bars
|
|
for vel in range(60, 36, -2):
|
|
bells.add(D.add(-12), Duration.WHOLE, velocity=vel)
|
|
rest_bars(bells, 3)
|
|
|
|
# Bars 57-64: alone again — the ending mirrors the beginning
|
|
bell_tolls(58, 50, 42)
|
|
|
|
# ── BAGPIPE — the ancient drone, enters bar 9 ────────────────
|
|
bagpipe = score.part("bagpipe", instrument="bagpipe", volume=0.2,
|
|
reverb=0.45, reverb_type="cathedral",
|
|
chorus=0.15, chorus_rate=0.08, chorus_depth=0.008,
|
|
pan=-0.2, humanize=0.06)
|
|
|
|
rest_bars(bagpipe, 8)
|
|
|
|
# Bars 9-16: drone enters — D and A, the ancient fifth
|
|
for vel in [25, 32, 38, 42, 45, 45, 42, 40]:
|
|
bagpipe.add(D.add(-12), Duration.HALF, velocity=vel)
|
|
bagpipe.add(A.add(-12), Duration.HALF, velocity=max(15, vel - 8))
|
|
|
|
# Bars 17-40: melody emerges from the drone
|
|
bagpipe_melody = [
|
|
(D, Duration.HALF, 52), (E, Duration.QUARTER, 48),
|
|
(F, Duration.QUARTER, 50),
|
|
(G, Duration.HALF, 55), (F, Duration.QUARTER, 48),
|
|
(E, Duration.QUARTER, 45),
|
|
(D, Duration.WHOLE, 52),
|
|
(None, Duration.HALF, 0), (A.add(-12), Duration.HALF, 48),
|
|
]
|
|
|
|
|
|
def bagpipe_phrase(boost=0):
|
|
for note, dur, vel in bagpipe_melody:
|
|
if note is None:
|
|
bagpipe.rest(dur)
|
|
else:
|
|
bagpipe.add(note, dur, velocity=min(70, vel + boost))
|
|
|
|
|
|
for _ in range(6):
|
|
bagpipe_phrase()
|
|
|
|
# Bars 41-48: full power — drone + melody together
|
|
bagpipe.set(volume=0.25)
|
|
for _ in range(2):
|
|
bagpipe_phrase(boost=8)
|
|
|
|
# Bars 49-56: drops to drone — choir takes over
|
|
bagpipe.set(volume=0.15)
|
|
for _ in range(8):
|
|
bagpipe.add(D.add(-12), Duration.HALF, velocity=38)
|
|
bagpipe.add(A.add(-12), Duration.HALF, velocity=30)
|
|
|
|
# Bars 57-64: fading
|
|
fade_bars(bagpipe, D.add(-12), [32, 28, 22, 18, 14, 10, 5, 0])
|
|
|
|
# ── MELLOTRON CHOIR — voices from the stone, enters bar 17 ────
|
|
choir = score.part("choir", instrument="mellotron_choir", volume=0.2,
|
|
reverb=0.55, reverb_type="cathedral",
|
|
chorus=0.2, chorus_rate=0.06, chorus_depth=0.01,
|
|
pan=0.1, humanize=0.08)
|
|
|
|
rest_bars(choir, 16)
|
|
|
|
# Bars 17-24: slow chords — the walls start singing
|
|
chord_bars(choir, prog, vel=40, repeats=2)
|
|
|
|
# Bars 25-40: fuller
|
|
chord_bars(choir, prog, vel=48, repeats=4)
|
|
|
|
# Bars 41-48: peak — full voice
|
|
choir.set(volume=0.28)
|
|
chord_bars(choir, prog2, vel=58, repeats=2)
|
|
|
|
# Bars 49-56: SOLO — the most human moment
|
|
choir.set(volume=0.32)
|
|
choir_melody = [
|
|
(A, Duration.HALF, 62), (Bb, Duration.QUARTER, 58),
|
|
(A, Duration.QUARTER, 55),
|
|
(G, Duration.HALF, 60), (F, Duration.QUARTER, 55),
|
|
(E, Duration.QUARTER, 52),
|
|
(F, Duration.DOTTED_HALF, 62), (E, Duration.QUARTER, 55),
|
|
(D, Duration.WHOLE, 58),
|
|
(F, Duration.QUARTER, 60), (G, Duration.QUARTER, 58),
|
|
(A, Duration.HALF, 65),
|
|
(Bb, Duration.QUARTER, 60), (A, Duration.QUARTER, 58),
|
|
(G, Duration.HALF, 55),
|
|
(F, Duration.HALF, 52), (E, Duration.HALF, 55),
|
|
(D, Duration.WHOLE, 58),
|
|
]
|
|
for note, dur, vel in choir_melody:
|
|
choir.add(note, dur, velocity=vel)
|
|
|
|
# Bars 57-64: fading — the voices retreat into stone
|
|
fade_bars(choir, prog[0], [48, 42, 35, 28, 22, 15, 10, 0])
|
|
|
|
# ── TIMPANI — thunder, enters bar 25 ──────────────────────────
|
|
timp = score.part("timpani", instrument="timpani", volume=0.5,
|
|
reverb=0.4, reverb_type="cathedral",
|
|
delay=0.08, delay_time=0.5, delay_feedback=0.1,
|
|
pan=-0.1, humanize=0.06)
|
|
|
|
rest_bars(timp, 24)
|
|
|
|
# Bars 25-32: sparse, heavy — like thunder in stone
|
|
timp.add(D.add(-12), Duration.WHOLE, velocity=75)
|
|
rest_bars(timp, 2)
|
|
timp.add(A.add(-24), Duration.WHOLE, velocity=68)
|
|
timp.rest(Duration.WHOLE)
|
|
timp.add(D.add(-12), Duration.HALF, velocity=72)
|
|
timp.add(D.add(-12), Duration.HALF, velocity=65)
|
|
timp.rest(Duration.WHOLE)
|
|
timp.add(D.add(-12), Duration.WHOLE, velocity=78)
|
|
|
|
|
|
def timp_roll(start, cap):
|
|
# Timpani roll — 16th notes crescendo
|
|
for i in range(16):
|
|
timp.add(D.add(-12), Duration.SIXTEENTH, velocity=min(cap, start + i * 3))
|
|
|
|
|
|
# Bars 33-40: more active — rolls
|
|
for bar in range(8):
|
|
if bar % 4 == 3:
|
|
timp_roll(50, cap=95)
|
|
else:
|
|
timp.add(D.add(-12), Duration.QUARTER, velocity=72)
|
|
timp.rest(Duration.DOTTED_HALF)
|
|
|
|
# Bars 41-48: peak — full rolls + accents
|
|
for bar in range(8):
|
|
if bar % 2 == 1:
|
|
timp_roll(55, cap=100)
|
|
else:
|
|
timp.add(D.add(-12), Duration.HALF, velocity=80)
|
|
timp.rest(Duration.QUARTER)
|
|
timp.add(A.add(-24), Duration.QUARTER, velocity=72)
|
|
|
|
# Bars 49-56: sparse again — under choir solo
|
|
for _ in range(4):
|
|
timp.add(D.add(-12), Duration.WHOLE, velocity=55)
|
|
timp.rest(Duration.WHOLE)
|
|
|
|
# Bars 57-64: fading
|
|
fade_bars(timp, D.add(-12), [48, 40, 32, 25, 0, 0, 0, 0])
|
|
|
|
# ── KICK — the deepest thunder, enters bar 25 ─────────────────
|
|
K = DrumSound.KICK
|
|
kick = score.part("kick", volume=0.7, humanize=0.03,
|
|
reverb=0.3, reverb_type="cathedral",
|
|
distortion=0.1, distortion_drive=2.0)
|
|
|
|
rest_bars(kick, 24)
|
|
|
|
# Bars 25-32: one hit per bar — synced with timpani
|
|
for _ in range(8):
|
|
kick.hit(K, Duration.QUARTER, velocity=95)
|
|
kick.rest(Duration.DOTTED_HALF)
|
|
|
|
# Bars 33-48: two hits per bar — heartbeat under the organ
|
|
for _ in range(16):
|
|
kick.hit(K, Duration.QUARTER, velocity=100)
|
|
kick.rest(Duration.QUARTER)
|
|
kick.hit(K, Duration.QUARTER, velocity=85)
|
|
kick.rest(Duration.QUARTER)
|
|
|
|
# Bars 49-56: sparse — under choir solo
|
|
for _ in range(8):
|
|
kick.hit(K, Duration.HALF, velocity=80)
|
|
kick.rest(Duration.HALF)
|
|
|
|
# Bars 57-64: fading
|
|
for vel in [72, 60, 48, 35, 22, 0, 0, 0]:
|
|
if vel > 0:
|
|
kick.hit(K, Duration.QUARTER, velocity=vel)
|
|
kick.rest(Duration.DOTTED_HALF)
|
|
else:
|
|
kick.rest(Duration.WHOLE)
|
|
|
|
# ── PIPE ORGAN — the full weight, enters bar 33 ──────────────
|
|
organ = score.part("organ", instrument="pipe_organ", volume=0.2,
|
|
reverb=0.5, reverb_type="cathedral",
|
|
chorus=0.1, chorus_rate=0.1, chorus_depth=0.005,
|
|
pan=0.05)
|
|
|
|
rest_bars(organ, 32)
|
|
|
|
# Bars 33-48: hymn chords — the weight of the building
|
|
chord_bars(organ, prog, vel=48, repeats=4)
|
|
|
|
# Bars 49-56: sustains under choir solo
|
|
organ.set(volume=0.15)
|
|
chord_bars(organ, prog, vel=38, repeats=2)
|
|
|
|
# Bars 57-64: fading
|
|
fade_bars(organ, prog[0], [35, 30, 25, 20, 15, 10, 5, 0])
|
|
|
|
# ── MELLOTRON STRINGS — bed, enters bar 33 ────────────────────
|
|
strings = score.part("strings", instrument="mellotron_strings", volume=0.12,
|
|
reverb=0.45, reverb_type="cathedral",
|
|
pan=-0.15)
|
|
|
|
rest_bars(strings, 32)
|
|
|
|
# Bars 33-56: tape strings — the warmth
|
|
chord_bars(strings, prog, vel=42, repeats=6)
|
|
|
|
# Bars 57-64: fading
|
|
fade_bars(strings, prog[0], [35, 28, 22, 18, 12, 8, 0, 0])
|
|
|
|
# ── SUB — the stone floor vibrating ──────────────────────────
|
|
sub = score.part("sub", synth="sine", envelope="pad", volume=0.3,
|
|
lowpass=100, sub_osc=0.3)
|
|
|
|
rest_bars(sub, 24)
|
|
|
|
for _ in range(32):
|
|
sub.add(D.add(-36), Duration.WHOLE, velocity=40)
|
|
|
|
fade_bars(sub, D.add(-36), [35, 28, 22, 15, 10, 5, 0, 0])
|
|
|
|
# ═════════════════════════════════════════════════════════════════
|
|
import sys
|
|
|
|
print(f"Key: {key}")
|
|
print(f"BPM: 60")
|
|
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 CATHEDRAL (live engine)...")
|
|
from pytheory_live.live import LiveEngine
|
|
engine = LiveEngine(buffer_size=1024)
|
|
engine.play_score(score)
|
|
else:
|
|
print("Playing CATHEDRAL...")
|
|
play_score(score)
|