Add Tape Memory (track 18), play.py loops back to picker

Tape Memory: Db minor, 90 BPM — mellotron flute, FM bells, drift,
crotales, granular texture, hard_sync, PWM, wavefold, ring_mod.
Theremin solo at the peak. Singing bowls + tingsha throughout.
Play.py now returns to track picker after playback finishes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 00:01:35 -04:00
parent 02e59afa93
commit 395571cc94
4 changed files with 573 additions and 44 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 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.
- Waveforms: extended to 3:16 with FM solo, saw/square duet, sine/triangle/PWM canon, finale. Added FM, PWM, wavefold synths.
- play.py: track picker loops back after playback, extracted _play_track()
## 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.
+2 -1
View File
@@ -33,7 +33,8 @@ Each track is a `.py` file. Run it to hear it.
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. 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.
17. **Waveforms** — F minor, 118 BPM. Pure synthesis showcase — percussive blips stacking, FM solo, saw/square duet in thirds, sine/triangle/PWM canon.
18. **Tape Memory** — Db minor, 90 BPM. Mellotron flute dreams surrounded by new synthesis. FM bells, drift oscillator, crotales, granular texture, hard_sync bass, PWM lead, wavefold grit, ring_mod aliens. Theremin solo at the peak. Singing bowls and tingsha throughout.
## Usage
+48 -43
View File
@@ -56,6 +56,7 @@ ALBUM_ORDER = [
"voices.py",
"intrusive.py",
"waveforms.py",
"tape_memory.py",
]
@@ -566,65 +567,29 @@ examples:
return p
def main():
parser = build_parser()
args = parser.parse_args()
# ── List ───────────────────────────────────────────────────────
if args.list:
list_tracks()
return
# ── Track picker when no score given ─────────────────────────
if not args.score:
path = pick_track()
if path is None:
return
else:
path = Path(args.score)
def _play_track(path, args):
"""Load, render, and play a single track."""
path = Path(path)
if not path.exists():
print(f"File not found: {path}")
sys.exit(1)
return
# ── Load ───────────────────────────────────────────────────────
score, mod = load_score(path)
title = get_title(mod, path)
# ── Post-build modifications ───────────────────────────────────
if args.bpm:
score.bpm = args.bpm
if args.pitch:
score.reference_pitch = args.pitch
if args.solo:
apply_solo(score, set(args.solo.split(",")))
if args.mute:
apply_mute(score, set(args.mute.split(",")))
apply_volume(score, args.volume)
# ── Info / parts ───────────────────────────────────────────────
if args.info:
show_info(score, mod, path)
return
if args.parts:
show_parts(score)
return
# ── Export to MIDI ─────────────────────────────────────────────
if args.midi:
score.save_midi(args.midi)
print(f"Exported MIDI -> {args.midi}")
return
# ── Show metadata before rendering ──────────────────────────────
show_info(score, mod, path)
print()
# ── Render ─────────────────────────────────────────────────────
from_sec = parse_time(args.from_time) if args.from_time else None
to_sec = parse_time(args.to_time) if args.to_time else None
@@ -637,7 +602,6 @@ def main():
loop=args.loop,
)
# ── Export to WAV ──────────────────────────────────────────────
if args.output:
save_wav(buf, sr, args.output)
duration_sec = len(buf) / sr
@@ -645,8 +609,6 @@ def main():
print(f"Exported WAV -> {args.output} ({m}:{s:02d})")
return
# ── Play ───────────────────────────────────────────────────────
# Build info lines for the player UI
info = []
parts = f"{score.time_signature} {score.bpm} BPM {len(score.parts)} parts"
extras = []
@@ -663,6 +625,49 @@ def main():
play_audio(buf, sr, title=title, info_lines=info, offset_sec=offset_sec)
def main():
parser = build_parser()
args = parser.parse_args()
# ── List ───────────────────────────────────────────────────────
if args.list:
list_tracks()
return
# ── Track picker when no score given ─────────────────────────
if not args.score:
while True:
path = pick_track()
if path is None:
return
_play_track(path, args)
return
else:
path = Path(args.score)
if not path.exists():
print(f"File not found: {path}")
sys.exit(1)
# ── Info / parts (no playback) ────────────────────────────────
if args.info or args.parts or args.midi:
score, mod = load_score(path)
if args.bpm:
score.bpm = args.bpm
if args.pitch:
score.reference_pitch = args.pitch
if args.info:
show_info(score, mod, path)
elif args.parts:
show_parts(score)
elif args.midi:
score.save_midi(args.midi)
print(f"Exported MIDI -> {args.midi}")
return
_play_track(path, args)
if __name__ == "__main__":
try:
main()
+517
View File
@@ -0,0 +1,517 @@
"""
TAPE MEMORY — mellotron dreams surrounded by new synthesis.
Warm analog tape meets FM bells, wavefold grit, drifting oscillators.
Everything pytheory can do in one track.
Db minor, 90 BPM.
"""
from pytheory import Key, Duration, Score, Tone, play_score
from pytheory.rhythm import DrumSound
key = Key("Db", "minor")
s = key.scale # Db Eb Fb Gb Ab Bbb Cb (enharmonic: C# D# E F# G# A B)
Db = s[0]; Eb = s[1]; Fb = s[2]; Gb = s[3]
Ab = s[4]; Bbb = s[5]; Cb = s[6]
# Use enharmonic names for readability
F = Fb; Bb = Bbb; C = Cb
score = Score("4/4", bpm=90)
K = DrumSound.KICK
S = DrumSound.SNARE
CH = DrumSound.CLOSED_HAT
prog = key.progression("i", "VII", "VI", "iv")
prog2 = key.progression("i", "v", "VI", "iv")
# ═══════════════════════════════════════════════════════════════════
# STRUCTURE (80 bars, ~5:20):
# Bars 1-8: Mellotron alone — warm, warbly, the tape
# Bars 9-16: FM bells join — metallic shimmer above
# Bars 17-24: Drift oscillator — analog warmth underneath
# Bars 25-32: Crotales + granular texture — crystalline layers
# Bars 33-40: Drums + hard_sync bass — the groove arrives
# Bars 41-48: PWM lead melody — wobbling, alive
# Bars 49-56: Wavefold + ring_mod — the dark textures
# Bars 57-64: Everything together — the full palette
# Bars 65-72: Mellotron solo reprise — back to the heart
# Bars 73-80: Dissolve — tape runs out
# ═══════════════════════════════════════════════════════════════════
# ── MELLOTRON — the heart, warm tape chords ───────────────────
mello = score.part("mellotron", instrument="mellotron_flute", volume=0.4,
reverb=0.4, reverb_type="taj_mahal",
delay=0.12, delay_time=0.333, delay_feedback=0.2,
pan=-0.1, humanize=0.1)
# Bars 1-8: alone — whole note chords, the tape warbles
for _ in range(2):
for chord in prog:
mello.add(chord, Duration.WHOLE, velocity=65)
# Bars 9-24: continues underneath everything
for _ in range(4):
for chord in prog2:
mello.add(chord, Duration.WHOLE, velocity=58)
# Bars 25-32: switches to arpeggiated
mello_arp = [Db, F, Ab, F, Db, Ab.add(-12), F.add(-12), Ab.add(-12)]
for _ in range(8):
for note in mello_arp:
mello.add(note, Duration.EIGHTH, velocity=55)
# Bars 33-56: chord pads under the groove
for _ in range(6):
for chord in prog:
mello.add(chord, Duration.WHOLE, velocity=52)
# Bars 57-64: full — louder for the peak
mello.set(volume=0.45)
for _ in range(2):
for chord in prog:
mello.add(chord, Duration.WHOLE, velocity=65)
# Bars 65-72: SOLO REPRISE — melody on mellotron
mello.set(volume=0.5)
mello_melody = [
(Ab, Duration.HALF, 72), (Gb, Duration.QUARTER, 65),
(F, Duration.QUARTER, 62),
(Eb, Duration.HALF, 68), (Db, Duration.QUARTER, 62),
(C.add(-12), Duration.QUARTER, 58),
(Db, Duration.DOTTED_HALF, 70), (Eb, Duration.QUARTER, 65),
(F, Duration.WHOLE, 68),
(Ab, Duration.QUARTER, 72), (Bb, Duration.QUARTER, 68),
(Ab, Duration.QUARTER, 65), (Gb, Duration.QUARTER, 62),
(F, Duration.HALF, 68), (Eb, Duration.HALF, 65),
(Db, Duration.WHOLE, 70),
(None, Duration.WHOLE, 0),
]
for note, dur, vel in mello_melody:
if note is None:
mello.rest(dur)
else:
mello.add(note, dur, velocity=vel)
# Bars 73-80: tape runs out — fading, slowing feeling
for vel in [58, 50, 42, 35, 28, 20, 12, 5]:
mello.add(Db, Duration.WHOLE, velocity=vel)
# ── FM — metallic bells, enters bar 9 ────────────────────────
fm = score.part("fm_bells", synth="fm", envelope="pluck", volume=0.25,
reverb=0.35, reverb_type="cathedral",
delay=0.2, delay_time=0.333, delay_feedback=0.25,
pan=0.3)
for _ in range(8):
fm.rest(Duration.WHOLE)
# Bars 9-16: bell-like blips — high, crystalline
fm_phrase = [
(Ab, Duration.QUARTER, 65), (None, Duration.QUARTER, 0),
(Bb, Duration.QUARTER, 60), (None, Duration.QUARTER, 0),
(None, Duration.QUARTER, 0), (F, Duration.QUARTER, 62),
(None, Duration.HALF, 0),
]
for _ in range(4):
for note, dur, vel in fm_phrase:
if note is None:
fm.rest(dur)
else:
fm.add(note, dur, velocity=vel)
# Bars 17-56: continues, evolving
for _ in range(20):
for note, dur, vel in fm_phrase:
if note is None:
fm.rest(dur)
else:
fm.add(note, dur, velocity=vel)
# Bars 57-64: peak
fm.set(volume=0.3)
for _ in range(4):
for note, dur, vel in fm_phrase:
if note is None:
fm.rest(dur)
else:
fm.add(note, dur, velocity=min(80, vel + 8))
# Bars 65-80: fading
for vel in [55, 48, 42, 35, 28, 22, 15, 10, 8, 5, 0, 0, 0, 0, 0, 0]:
if vel > 0:
fm.add(Ab, Duration.QUARTER, velocity=vel)
fm.rest(Duration.DOTTED_HALF)
else:
fm.rest(Duration.WHOLE)
# ── DRIFT — analog warmth, enters bar 17 ─────────────────────
drift = score.part("drift", synth="drift", envelope="pad", volume=0.18,
reverb=0.4, reverb_type="taj_mahal",
chorus=0.3, chorus_rate=0.04, chorus_depth=0.012,
pan=-0.25)
for _ in range(16):
drift.rest(Duration.WHOLE)
# Bars 17-72: slow drifting drone — the analog warmth
for _ in range(56):
drift.add(Db.add(-12), Duration.WHOLE, velocity=42)
# Bars 73-80: fading
for vel in [35, 28, 22, 15, 10, 6, 3, 0]:
if vel > 0:
drift.add(Db.add(-12), Duration.WHOLE, velocity=vel)
else:
drift.rest(Duration.WHOLE)
# ── CROTALES — crystalline, enters bar 25 ─────────────────────
crot = score.part("crotales", instrument="crotales", volume=0.2,
reverb=0.5, reverb_type="taj_mahal",
delay=0.2, delay_time=0.667, delay_feedback=0.25,
pan=0.35)
for _ in range(24):
crot.rest(Duration.WHOLE)
# Bars 25-32: sparse strikes — like tiny bells in the distance
crot_strikes = {25: (Ab, 58), 27: (F, 52), 29: (Bb, 55), 31: (Db, 50)}
for bar in range(25, 33):
if bar in crot_strikes:
note, vel = crot_strikes[bar]
crot.add(note, Duration.WHOLE, velocity=vel)
else:
crot.rest(Duration.WHOLE)
# Bars 33-56: continues sparse
for bar in range(33, 57):
if bar % 4 == 1:
crot.add(Ab, Duration.WHOLE, velocity=48)
elif bar % 6 == 0:
crot.add(F, Duration.WHOLE, velocity=45)
else:
crot.rest(Duration.WHOLE)
# Bars 57-80: fading
for bar in range(57, 81):
if bar % 5 == 0 and bar < 73:
crot.add(Db, Duration.WHOLE, velocity=max(25, 50 - (bar - 57)))
else:
crot.rest(Duration.WHOLE)
# ── GRANULAR — texture, enters bar 25 ─────────────────────────
grain = score.part("grain", instrument="granular_texture", volume=0.1,
reverb=0.45, reverb_type="taj_mahal",
delay=0.1, delay_time=0.5, delay_feedback=0.15,
pan=-0.35)
for _ in range(24):
grain.rest(Duration.WHOLE)
# Bars 25-64: evolving texture — slowly shifting notes
grain_notes = [Db, F, Ab, Eb, Bb, Gb, F, Db]
for note in grain_notes:
for _ in range(5):
grain.add(note, Duration.WHOLE, velocity=35)
# Bars 65-80: fading
for vel in [30, 25, 22, 18, 15, 12, 8, 5, 0, 0, 0, 0, 0, 0, 0, 0]:
if vel > 0:
grain.add(Db, Duration.WHOLE, velocity=vel)
else:
grain.rest(Duration.WHOLE)
# ── HARD_SYNC — bass, enters bar 33 ──────────────────────────
sync = score.part("hard_sync", synth="hard_sync", volume=0.3,
lowpass=800,
distortion=0.15, distortion_drive=2.0,
reverb=0.15, reverb_type="spring",
delay=0.08, delay_time=0.333, delay_feedback=0.1,
pan=0.1)
for _ in range(32):
sync.rest(Duration.WHOLE)
# Bars 33-64: bass line — hard_sync has that aggressive buzz
roots = [Db.add(-12), Bb.add(-24), Gb.add(-12), Ab.add(-12)]
for _ in range(8):
for root in roots:
sync.add(root, Duration.HALF, velocity=72)
sync.rest(Duration.HALF)
# Bars 65-80: fading
for vel in [62, 52, 42, 32, 22, 15, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0]:
if vel > 0:
sync.add(Db.add(-12), Duration.HALF, velocity=vel)
sync.rest(Duration.HALF)
else:
sync.rest(Duration.WHOLE)
# ── DRUMS — enters bar 33 ────────────────────────────────────
kick = score.part("kick", volume=0.55, humanize=0.03)
snare = score.part("snare", volume=0.35, humanize=0.04,
reverb=0.15, delay=0.05, delay_time=0.333,
delay_feedback=0.08, pan=0.05)
hats = score.part("hats", volume=0.2, pan=0.15, humanize=0.04)
for _ in range(32):
kick.rest(Duration.WHOLE)
snare.rest(Duration.WHOLE)
hats.rest(Duration.WHOLE)
# Bars 33-64: groove
for _ in range(32):
kick.hit(K, Duration.QUARTER, velocity=98)
kick.rest(Duration.EIGHTH)
kick.hit(K, Duration.EIGHTH, velocity=82)
kick.hit(K, Duration.QUARTER, velocity=92)
kick.rest(Duration.QUARTER)
snare.rest(Duration.QUARTER)
snare.hit(S, Duration.QUARTER, velocity=88)
snare.rest(Duration.QUARTER)
snare.hit(S, Duration.QUARTER, velocity=90)
for beat in range(4):
hats.hit(CH, Duration.EIGHTH, velocity=58)
hats.hit(CH, Duration.EIGHTH, velocity=35)
# Bars 65-72: lighter under mellotron solo
for _ in range(8):
kick.hit(K, Duration.QUARTER, velocity=78)
kick.rest(Duration.DOTTED_HALF)
snare.rest(Duration.QUARTER)
snare.hit(S, Duration.QUARTER, velocity=65)
snare.rest(Duration.HALF)
for beat in range(4):
hats.rest(Duration.EIGHTH)
hats.hit(CH, Duration.EIGHTH, velocity=42)
# Bars 73-80: fading
for vel in [68, 55, 42, 30, 20, 0, 0, 0]:
if vel > 0:
kick.hit(K, Duration.QUARTER, velocity=vel)
kick.rest(Duration.DOTTED_HALF)
snare.rest(Duration.WHOLE)
hats.hit(CH, Duration.QUARTER, velocity=max(15, vel - 30))
hats.rest(Duration.DOTTED_HALF)
else:
kick.rest(Duration.WHOLE)
snare.rest(Duration.WHOLE)
hats.rest(Duration.WHOLE)
# ── PWM — lead melody, enters bar 41 ─────────────────────────
pwm = score.part("pwm_lead", synth="pwm", volume=0.35,
reverb=0.25, reverb_decay=1.2,
delay=0.15, delay_time=0.333, delay_feedback=0.2,
pan=-0.2, humanize=0.06)
for _ in range(40):
pwm.rest(Duration.WHOLE)
# Bars 41-48: melody — the PWM wobble makes each note alive
pwm_melody = [
(Ab, Duration.HALF, 72), (Gb, Duration.QUARTER, 65),
(F, Duration.QUARTER, 68),
(Eb, Duration.HALF, 70), (F, Duration.QUARTER, 65),
(Gb, Duration.QUARTER, 68),
(Ab, Duration.DOTTED_HALF, 75), (Gb, Duration.QUARTER, 68),
(F, Duration.WHOLE, 70),
(Eb, Duration.QUARTER, 68), (F, Duration.QUARTER, 72),
(Gb, Duration.HALF, 70),
(Ab, Duration.QUARTER, 75), (Bb, Duration.QUARTER, 72),
(Ab, Duration.HALF, 70),
(Gb, Duration.HALF, 68), (F, Duration.HALF, 72),
(Db, Duration.WHOLE, 70),
]
for note, dur, vel in pwm_melody:
pwm.add(note, dur, velocity=vel)
# Bars 49-56: repeats with variation
for note, dur, vel in pwm_melody:
pwm.add(note, dur, velocity=max(30, vel - 5))
# Bars 57-64: peak — louder
pwm.set(volume=0.4)
for note, dur, vel in pwm_melody:
pwm.add(note, dur, velocity=min(90, vel + 5))
# Bars 65-80: fading
for vel in [60, 52, 44, 38, 30, 22, 15, 8, 0, 0, 0, 0, 0, 0, 0, 0]:
if vel > 0:
pwm.add(Db, Duration.HALF, velocity=vel)
pwm.rest(Duration.HALF)
else:
pwm.rest(Duration.WHOLE)
# ── WAVEFOLD — dark texture, enters bar 49 ───────────────────
wfold = score.part("wavefold", synth="wavefold", envelope="pluck", volume=0.15,
lowpass=3000,
reverb=0.2, reverb_decay=1.0,
delay=0.1, delay_time=0.167, delay_feedback=0.15,
pan=0.25)
for _ in range(48):
wfold.rest(Duration.WHOLE)
# Bars 49-64: dark rhythmic texture — the grit
wf_pattern = [
(Db, Duration.SIXTEENTH, 62), (None, Duration.SIXTEENTH, 0),
(F, Duration.SIXTEENTH, 58), (None, Duration.SIXTEENTH, 0),
(None, Duration.EIGHTH, 0),
(Ab, Duration.SIXTEENTH, 60), (None, Duration.SIXTEENTH, 0),
(None, Duration.EIGHTH, 0),
(Eb, Duration.SIXTEENTH, 55), (None, Duration.SIXTEENTH, 0),
(None, Duration.QUARTER, 0),
]
for _ in range(16):
for note, dur, vel in wf_pattern:
if note is None:
wfold.rest(dur)
else:
wfold.add(note, dur, velocity=vel)
# Bars 65-80: fading
for rep in range(16):
off = rep * -4
for note, dur, vel in wf_pattern:
if note is None:
wfold.rest(dur)
else:
wfold.add(note, dur, velocity=max(12, vel + off))
# ── RING MOD — alien texture, enters bar 49 ──────────────────
ring = score.part("ring_mod", synth="ring_mod", envelope="pluck", volume=0.1,
reverb=0.3, reverb_type="cathedral",
delay=0.15, delay_time=0.5, delay_feedback=0.2,
pan=-0.4)
for _ in range(48):
ring.rest(Duration.WHOLE)
# Bars 49-64: sparse alien blips — inharmonic, unsettling beauty
ring_hits = {49: (Ab, 50), 51: (F, 45), 53: (Db, 48), 55: (Eb, 42),
57: (Ab, 52), 59: (Bb, 48), 61: (F, 50), 63: (Db, 45)}
for bar in range(49, 65):
if bar in ring_hits:
note, vel = ring_hits[bar]
ring.add(note, Duration.WHOLE, velocity=vel)
else:
ring.rest(Duration.WHOLE)
# Bars 65-80: fading
for vel in [38, 32, 28, 22, 18, 12, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0]:
if vel > 0:
ring.add(Db, Duration.WHOLE, velocity=vel)
else:
ring.rest(Duration.WHOLE)
# ── SINGING BOWL — transition markers ─────────────────────────
bowl = score.part("bowl", instrument="singing_bowl", volume=0.3,
reverb=0.7, reverb_type="taj_mahal",
delay=0.15, delay_time=0.667, delay_feedback=0.2,
pan=0.2)
section_bars = {1: 62, 9: 58, 17: 55, 25: 60, 33: 65, 41: 58,
49: 62, 57: 68, 65: 60, 73: 50}
for bar in range(1, 81):
if bar in section_bars:
bowl.add(Db.add(-24), Duration.WHOLE, velocity=section_bars[bar])
else:
bowl.rest(Duration.WHOLE)
# ── TINGSHA — crystalline accents ─────────────────────────────
tingsha = score.part("tingsha", instrument="tingsha", volume=0.18,
reverb=0.5, reverb_type="taj_mahal",
delay=0.2, delay_time=1.0, delay_feedback=0.2,
pan=-0.3)
tingsha_bars = {4: (Ab, 50), 12: (F, 45), 20: (Db, 48),
28: (Bb, 45), 36: (Ab, 50), 52: (F, 45),
60: (Db, 52), 68: (Ab, 48)}
for bar in range(1, 81):
if bar in tingsha_bars:
note, vel = tingsha_bars[bar]
tingsha.add(note, Duration.WHOLE, velocity=vel)
else:
tingsha.rest(Duration.WHOLE)
# ── THEREMIN — emotional peak solo, bars 57-64 ────────────────
theremin = score.part("theremin", instrument="theremin", volume=0.35,
reverb=0.35, reverb_type="taj_mahal",
delay=0.15, delay_time=0.333, delay_feedback=0.2,
pan=0.15, humanize=0.06)
for _ in range(56):
theremin.rest(Duration.WHOLE)
# Bars 57-58: entrance — rising from the texture, one held note
theremin.add(Ab, Duration.WHOLE, velocity=62, bend=0.5)
theremin.add(Db, Duration.HALF, velocity=68, bend=-0.25)
theremin.add(Eb, Duration.HALF, velocity=65)
# Bars 59-60: the solo opens up — singing, bending
theremin.add(Ab, Duration.QUARTER, velocity=75, bend=0.5)
theremin.add(Gb, Duration.EIGHTH, velocity=68)
theremin.add(F, Duration.EIGHTH, velocity=65)
theremin.add(Eb, Duration.HALF, velocity=72, bend=-0.25)
theremin.add(F, Duration.QUARTER, velocity=70, bend=0.5)
theremin.add(Ab, Duration.QUARTER, velocity=78)
theremin.add(Bb, Duration.HALF, velocity=80, bend=-0.5)
theremin.add(Ab, Duration.HALF, velocity=72)
# Bars 61-62: climax — the highest, most exposed moment
theremin.add(Bb, Duration.QUARTER, velocity=82, bend=1.0)
theremin.add(Ab, Duration.QUARTER, velocity=78, bend=-0.5)
theremin.add(Gb, Duration.QUARTER, velocity=72, bend=0.5)
theremin.add(Ab, Duration.QUARTER, velocity=80, bend=1.0)
# Descending — the release
theremin.add(Gb, Duration.QUARTER, velocity=75, bend=0.25)
theremin.add(F, Duration.QUARTER, velocity=70)
theremin.add(Eb, Duration.QUARTER, velocity=65, bend=-0.25)
theremin.add(Db, Duration.QUARTER, velocity=62)
# Bars 63-64: fading — one last held note
theremin.add(Db, Duration.WHOLE, velocity=58, bend=0.15)
theremin.add(Db, Duration.WHOLE, velocity=42)
# Bars 65-80: gone
for _ in range(16):
theremin.rest(Duration.WHOLE)
# ── SUB — enters bar 33 ──────────────────────────────────────
sub = score.part("sub", synth="sine", envelope="pad", volume=0.45,
lowpass=150, distortion=0.12, distortion_drive=2.0,
sub_osc=0.4, sidechain=0.3)
for _ in range(32):
sub.rest(Duration.WHOLE)
sub_roots = [Db.add(-24), Bb.add(-24), Gb.add(-24), Ab.add(-24)]
for _ in range(8):
for root in sub_roots:
sub.add(root, Duration.WHOLE, velocity=35)
# Bars 65-80: just the root, fading
for vel in [32, 28, 25, 22, 18, 15, 12, 8, 5, 0, 0, 0, 0, 0, 0, 0]:
if vel > 0:
sub.add(Db.add(-24), Duration.WHOLE, velocity=vel)
else:
sub.rest(Duration.WHOLE)
# ═════════════════════════════════════════════════════════════════
import sys
print(f"Key: {key}")
print(f"BPM: 90")
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 TAPE MEMORY (live engine)...")
from pytheory_live.live import LiveEngine
engine = LiveEngine(buffer_size=1024)
engine.play_score(score)
else:
print("Playing TAPE MEMORY...")
play_score(score)