From a9dbff25d33c80920dfbfcadc730369b5d53cfa4 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Fri, 3 Apr 2026 00:43:18 -0400 Subject: [PATCH] Add Music Box Factory (20) + Cathedral (21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Music Box Factory: G major, 108 BPM — 8 tuned percussion instruments. Kalimba, vibraphone, celesta, marimba, glockenspiel, xylophone, crotales, timpani. No synths, no strings. Cathedral: D minor, 60 BPM — tubular bells in taj_mahal, bagpipe drone, mellotron choir, timpani thunder, pipe organ, kick in cathedral reverb. Mellotron choir solo at the heart. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 + README.md | 4 +- play.py | 2 + tracks/cathedral.py | 335 ++++++++++++++++++++++++++++ tracks/music_box_factory.py | 424 ++++++++++++++++++++++++++++++++++++ 5 files changed, 766 insertions(+), 1 deletion(-) create mode 100644 tracks/cathedral.py create mode 100644 tracks/music_box_factory.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 42613e5..6c1750f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 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 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. - **Track 19: Emergence** — E minor, 100 BPM. Singing bowls + tingsha opening, didgeridoo, mellotron flute, sitar 16th arps building to 32nd shreds, synths emerge at bar 33, both worlds collide, mellotron solo, bowls alone ending. - 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() diff --git a/README.md b/README.md index a05b9bf..fb7046d 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ Each track is a `.py` file. Run it to hear it. 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 — 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, ring_mod. Theremin solo at the peak. -19. **Emergence** — E minor, 100 BPM. The acoustic world births the electronic one. Singing bowls, tingsha, didgeridoo, mellotron flute, sitar 16th arps with 32nd shreds — then synths emerge. Saw, FM, square, supersaw. Both worlds collide at the peak. Bowls alone at the end. +19. **Emergence** — E minor, 100 BPM. Acoustic births electronic. Singing bowls, tingsha, didgeridoo, mellotron flute, sitar 16th arps with 32nd shreds, then synths emerge. Both worlds collide at the peak. +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 reverb, bagpipe drone, mellotron choir, timpani thunder with 16th note rolls, pipe organ hymn, kick in cathedral reverb. Mellotron choir solo. Bells alone at the end. ## Usage diff --git a/play.py b/play.py index d82ab61..0e1a3f3 100644 --- a/play.py +++ b/play.py @@ -58,6 +58,8 @@ ALBUM_ORDER = [ "waveforms.py", "tape_memory.py", "emergence.py", + "music_box_factory.py", + "cathedral.py", ] diff --git a/tracks/cathedral.py b/tracks/cathedral.py new file mode 100644 index 0000000..38cce75 --- /dev/null +++ b/tracks/cathedral.py @@ -0,0 +1,335 @@ +""" +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, 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=60) + +prog = key.progression("i", "iv", "VII", "i") +prog2 = key.progression("i", "VI", "iv", "V") + +# ═══════════════════════════════════════════════════════════════════ +# 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) + +# Bars 1-8: alone — one strike, let it ring, another +bells.add(D.add(-12), Duration.WHOLE, velocity=65) +bells.rest(Duration.WHOLE) +bells.rest(Duration.WHOLE) +bells.add(A.add(-12), Duration.WHOLE, velocity=58) +bells.rest(Duration.WHOLE) +bells.rest(Duration.WHOLE) +bells.add(D.add(-12), Duration.WHOLE, velocity=62) +bells.rest(Duration.WHOLE) + +# Bars 9-56: every 4 bars +for section in range(12): + vel = max(30, 60 - section * 2) + bells.add(D.add(-12), Duration.WHOLE, velocity=vel) + bells.rest(Duration.WHOLE) + bells.rest(Duration.WHOLE) + bells.rest(Duration.WHOLE) + +# Bars 57-64: alone again — the ending mirrors the beginning +bells.add(D.add(-12), Duration.WHOLE, velocity=58) +bells.rest(Duration.WHOLE) +bells.rest(Duration.WHOLE) +bells.add(A.add(-12), Duration.WHOLE, velocity=50) +bells.rest(Duration.WHOLE) +bells.rest(Duration.WHOLE) +bells.add(D.add(-12), Duration.WHOLE, velocity=42) +bells.rest(Duration.WHOLE) + +# ── 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) + +for _ in range(8): + bagpipe.rest(Duration.WHOLE) + +# 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), +] +for _ in range(6): + for note, dur, vel in bagpipe_melody: + if note is None: + bagpipe.rest(dur) + else: + bagpipe.add(note, dur, velocity=vel) + +# Bars 41-48: full power — drone + melody together +bagpipe.set(volume=0.25) +for _ in range(2): + for note, dur, vel in bagpipe_melody: + if note is None: + bagpipe.rest(dur) + else: + bagpipe.add(note, dur, velocity=min(70, vel + 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 +for vel in [32, 28, 22, 18, 14, 10, 5, 0]: + if vel > 0: + bagpipe.add(D.add(-12), Duration.WHOLE, velocity=vel) + else: + bagpipe.rest(Duration.WHOLE) + +# ── 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) + +for _ in range(16): + choir.rest(Duration.WHOLE) + +# Bars 17-24: slow chords — the walls start singing +for _ in range(2): + for chord in prog: + choir.add(chord, Duration.WHOLE, velocity=40) + +# Bars 25-40: fuller +for _ in range(4): + for chord in prog: + choir.add(chord, Duration.WHOLE, velocity=48) + +# Bars 41-48: peak — full voice +choir.set(volume=0.28) +for _ in range(2): + for chord in prog2: + choir.add(chord, Duration.WHOLE, velocity=58) + +# 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 +for vel in [48, 42, 35, 28, 22, 15, 10, 0]: + if vel > 0: + choir.add(prog[0], Duration.WHOLE, velocity=vel) + else: + choir.rest(Duration.WHOLE) + +# ── 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) + +for _ in range(24): + timp.rest(Duration.WHOLE) + +# Bars 25-32: sparse, heavy — like thunder in stone +timp.add(D.add(-12), Duration.WHOLE, velocity=75) +timp.rest(Duration.WHOLE) +timp.rest(Duration.WHOLE) +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) + +# Bars 33-40: more active — rolls +for bar in range(8): + if bar % 4 == 3: + # Timpani roll — 16th notes crescendo + for i in range(16): + timp.add(D.add(-12), Duration.SIXTEENTH, velocity=min(95, 50 + i * 3)) + 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: + for i in range(16): + timp.add(D.add(-12), Duration.SIXTEENTH, velocity=min(100, 55 + i * 3)) + 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 +for vel in [48, 40, 32, 25, 0, 0, 0, 0]: + if vel > 0: + timp.add(D.add(-12), Duration.WHOLE, velocity=vel) + else: + timp.rest(Duration.WHOLE) + +# ── 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) + +for _ in range(24): + kick.rest(Duration.WHOLE) + +# 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) + +for _ in range(32): + organ.rest(Duration.WHOLE) + +# Bars 33-48: hymn chords — the weight of the building +for _ in range(4): + for chord in prog: + organ.add(chord, Duration.WHOLE, velocity=48) + +# Bars 49-56: sustains under choir solo +organ.set(volume=0.15) +for _ in range(2): + for chord in prog: + organ.add(chord, Duration.WHOLE, velocity=38) + +# Bars 57-64: fading +for vel in [35, 30, 25, 20, 15, 10, 5, 0]: + if vel > 0: + organ.add(prog[0], Duration.WHOLE, velocity=vel) + else: + organ.rest(Duration.WHOLE) + +# ── 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) + +for _ in range(32): + strings.rest(Duration.WHOLE) + +# Bars 33-56: tape strings — the warmth +for _ in range(6): + for chord in prog: + strings.add(chord, Duration.WHOLE, velocity=42) + +# Bars 57-64: fading +for vel in [35, 28, 22, 18, 12, 8, 0, 0]: + if vel > 0: + strings.add(prog[0], Duration.WHOLE, velocity=vel) + else: + strings.rest(Duration.WHOLE) + +# ── SUB — the stone floor vibrating ────────────────────────── +sub = score.part("sub", synth="sine", envelope="pad", volume=0.3, + lowpass=100, sub_osc=0.3) + +for _ in range(24): + sub.rest(Duration.WHOLE) + +for _ in range(32): + sub.add(D.add(-36), Duration.WHOLE, velocity=40) + +for vel in [35, 28, 22, 15, 10, 5, 0, 0]: + if vel > 0: + sub.add(D.add(-36), Duration.WHOLE, velocity=vel) + else: + sub.rest(Duration.WHOLE) + +# ═════════════════════════════════════════════════════════════════ +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) diff --git a/tracks/music_box_factory.py b/tracks/music_box_factory.py new file mode 100644 index 0000000..8fdd626 --- /dev/null +++ b/tracks/music_box_factory.py @@ -0,0 +1,424 @@ +""" +MUSIC BOX FACTORY — tuned percussion only. +Kalimba, vibraphone, celesta, marimba, glockenspiel, xylophone, crotales. +No synths. No strings. Just metal and wood and keys. +G major, 108 BPM. +""" + +from pytheory import Key, Duration, Score, Tone, play_score +from pytheory.rhythm import DrumSound + +key = Key("G", "major") +s = key.scale # G A B C D E F# + +G = s[0]; A = s[1]; B = s[2]; C = s[3] +D = s[4]; E = s[5]; Fs = s[6] + +score = Score("4/4", bpm=108) + +prog = key.progression("I", "vi", "IV", "V") + +# ═══════════════════════════════════════════════════════════════════ +# STRUCTURE (72 bars, ~4:00): +# Bars 1-8: Kalimba alone — thumb piano, intimate +# Bars 9-16: Vibraphone joins — jazz shimmer, motor wobble +# Bars 17-24: Celesta — ethereal, high, Tchaikovsky's ghost +# Bars 25-32: Marimba — warm wood, the low end +# Bars 33-40: Glockenspiel — bright, cutting through +# Bars 41-48: All together — the factory floor +# Bars 49-56: Xylophone + crotales — the brightest moment +# Bars 57-64: Kalimba melody reprise — over everything +# Bars 65-72: Winding down — one by one they stop +# ═══════════════════════════════════════════════════════════════════ + +# ── KALIMBA — the seed, thumb piano ─────────────────────────── +kalimba = score.part("kalimba", instrument="kalimba", volume=0.45, + reverb=0.25, reverb_type="taj_mahal", + delay=0.15, delay_time=0.278, delay_feedback=0.2, + pan=-0.15, humanize=0.1) + +# Bars 1-8: alone — simple, percussive, melodic +kalimba_phrase_a = [ + (G, Duration.EIGHTH, 72), (None, Duration.EIGHTH, 0), + (B, Duration.EIGHTH, 65), (D, Duration.EIGHTH, 68), + (None, Duration.EIGHTH, 0), (B, Duration.EIGHTH, 62), + (G, Duration.EIGHTH, 70), (None, Duration.EIGHTH, 0), +] +kalimba_phrase_b = [ + (E, Duration.EIGHTH, 68), (None, Duration.EIGHTH, 0), + (G, Duration.EIGHTH, 65), (B, Duration.EIGHTH, 70), + (A, Duration.EIGHTH, 62), (None, Duration.EIGHTH, 0), + (G, Duration.EIGHTH, 68), (None, Duration.EIGHTH, 0), +] +for _ in range(4): + for note, dur, vel in kalimba_phrase_a: + if note is None: + kalimba.rest(dur) + else: + kalimba.add(note, dur, velocity=vel) + for note, dur, vel in kalimba_phrase_b: + if note is None: + kalimba.rest(dur) + else: + kalimba.add(note, dur, velocity=vel) + +# Bars 9-56: continues — the constant heartbeat +for _ in range(24): + for note, dur, vel in kalimba_phrase_a: + if note is None: + kalimba.rest(dur) + else: + kalimba.add(note, dur, velocity=vel) + for note, dur, vel in kalimba_phrase_b: + if note is None: + kalimba.rest(dur) + else: + kalimba.add(note, dur, velocity=vel) + +# Bars 57-64: REPRISE — melody sings above everything +kalimba.set(volume=0.55) +kalimba_melody = [ + (D, Duration.QUARTER, 78), (E, Duration.EIGHTH, 72), + (D, Duration.EIGHTH, 68), (B, Duration.HALF, 75), + (A, Duration.QUARTER, 70), (G, Duration.EIGHTH, 65), + (Fs, Duration.EIGHTH, 62), (G, Duration.HALF, 72), + (B, Duration.QUARTER, 75), (D, Duration.QUARTER, 72), + (E, Duration.HALF, 78), + (D, Duration.QUARTER, 72), (B, Duration.QUARTER, 68), + (A, Duration.QUARTER, 65), (G, Duration.QUARTER, 70), + (G, Duration.WHOLE, 72), + (None, Duration.WHOLE, 0), +] +for note, dur, vel in kalimba_melody: + if note is None: + kalimba.rest(dur) + else: + kalimba.add(note, dur, velocity=vel) + +# Bars 65-72: last one playing — fading +kalimba.set(volume=0.4) +for rep in range(4): + off = rep * -12 + for note, dur, vel in kalimba_phrase_a: + if note is None: + kalimba.rest(dur) + else: + kalimba.add(note, dur, velocity=max(20, vel + off)) + for note, dur, vel in kalimba_phrase_b: + if note is None: + kalimba.rest(dur) + else: + kalimba.add(note, dur, velocity=max(20, vel + off)) + +# ── VIBRAPHONE — jazz shimmer, enters bar 9 ────────────────── +vib = score.part("vibraphone", instrument="vibraphone", volume=0.35, + reverb=0.3, reverb_type="cathedral", + delay=0.12, delay_time=0.556, delay_feedback=0.2, + pan=0.25, humanize=0.08) + +for _ in range(8): + vib.rest(Duration.WHOLE) + +# Bars 9-16: sustained chords — the motor wobble gives life +for _ in range(2): + for chord in prog: + vib.add(chord, Duration.WHOLE, velocity=55) + +# Bars 17-40: continues — warm bed under everything +for _ in range(6): + for chord in prog: + vib.add(chord, Duration.WHOLE, velocity=52) + +# Bars 41-48: arpeggiated — the factory speeds up +vib_arp = [G, B, D, B, G, D.add(-12), B.add(-12), D.add(-12)] +for _ in range(8): + for note in vib_arp: + vib.add(note, Duration.EIGHTH, velocity=60) + +# Bars 49-56: peak chords +for _ in range(2): + for chord in prog: + vib.add(chord, Duration.WHOLE, velocity=62) + +# Bars 57-64: under kalimba melody +for _ in range(2): + for chord in prog: + vib.add(chord, Duration.WHOLE, velocity=50) + +# Bars 65-72: fading +for vel in [45, 40, 35, 30, 25, 18, 10, 0]: + if vel > 0: + vib.add(prog[0], Duration.WHOLE, velocity=vel) + else: + vib.rest(Duration.WHOLE) + +# ── CELESTA — ethereal, enters bar 17 ──────────────────────── +celesta = score.part("celesta", instrument="celesta", volume=0.3, + reverb=0.35, reverb_type="taj_mahal", + delay=0.15, delay_time=0.278, delay_feedback=0.25, + pan=-0.3, humanize=0.08) + +for _ in range(16): + celesta.rest(Duration.WHOLE) + +# Bars 17-24: high, sparkling countermelody +celesta_phrase = [ + (None, Duration.QUARTER, 0), + (D, Duration.EIGHTH, 62), (E, Duration.EIGHTH, 58), + (None, Duration.QUARTER, 0), + (B, Duration.EIGHTH, 60), (None, Duration.EIGHTH, 0), + (None, Duration.QUARTER, 0), + (E, Duration.EIGHTH, 62), (D, Duration.EIGHTH, 58), + (B, Duration.EIGHTH, 55), (None, Duration.EIGHTH, 0), + (None, Duration.QUARTER, 0), +] +for _ in range(8): + for note, dur, vel in celesta_phrase: + if note is None: + celesta.rest(dur) + else: + celesta.add(note, dur, velocity=vel) + +# Bars 25-56: continues +for _ in range(32): + for note, dur, vel in celesta_phrase: + if note is None: + celesta.rest(dur) + else: + celesta.add(note, dur, velocity=vel) + +# Bars 57-72: fading +for rep in range(8): + off = rep * -6 + for note, dur, vel in celesta_phrase: + if note is None: + celesta.rest(dur) + else: + celesta.add(note, dur, velocity=max(15, vel + off)) +for _ in range(8): + celesta.rest(Duration.WHOLE) + +# ── MARIMBA — warm wood, the bass, enters bar 25 ───────────── +marimba = score.part("marimba", instrument="marimba", volume=0.4, + reverb=0.2, reverb_decay=1.0, + delay=0.08, delay_time=0.278, delay_feedback=0.1, + pan=0.1, humanize=0.08) + +for _ in range(24): + marimba.rest(Duration.WHOLE) + +# Bars 25-32: low register — the warm bass of the ensemble +marimba_bass = [ + (G.add(-12), Duration.QUARTER, 72), (None, Duration.EIGHTH, 0), + (G.add(-12), Duration.EIGHTH, 62), (B.add(-12), Duration.QUARTER, 68), + (None, Duration.QUARTER, 0), + (D, Duration.QUARTER, 65), (None, Duration.EIGHTH, 0), + (B.add(-12), Duration.EIGHTH, 60), (G.add(-12), Duration.HALF, 70), +] +for _ in range(8): + for note, dur, vel in marimba_bass: + if note is None: + marimba.rest(dur) + else: + marimba.add(note, dur, velocity=vel) + +# Bars 33-56: continues — the floor +for _ in range(24): + for note, dur, vel in marimba_bass: + if note is None: + marimba.rest(dur) + else: + marimba.add(note, dur, velocity=vel) + +# Bars 57-72: fading +for rep in range(8): + off = rep * -8 + for note, dur, vel in marimba_bass: + if note is None: + marimba.rest(dur) + else: + marimba.add(note, dur, velocity=max(20, vel + off)) +for _ in range(8): + marimba.rest(Duration.WHOLE) + +# ── GLOCKENSPIEL — bright, cutting, enters bar 33 ──────────── +glock = score.part("glockenspiel", instrument="glockenspiel", volume=0.2, + reverb=0.3, reverb_type="cathedral", + delay=0.12, delay_time=0.139, delay_feedback=0.15, + pan=-0.4, humanize=0.06) + +for _ in range(32): + glock.rest(Duration.WHOLE) + +# Bars 33-40: high bright hits — sparse, like light catching metal +glock_hits = [ + (D, Duration.QUARTER, 58), (None, Duration.DOTTED_HALF, 0), + (None, Duration.HALF, 0), (E, Duration.QUARTER, 55), + (None, Duration.QUARTER, 0), + (B, Duration.QUARTER, 60), (None, Duration.DOTTED_HALF, 0), + (None, Duration.WHOLE, 0), + (D, Duration.QUARTER, 55), (None, Duration.QUARTER, 0), + (G, Duration.QUARTER, 58), (None, Duration.QUARTER, 0), + (None, Duration.WHOLE, 0), + (None, Duration.HALF, 0), (Fs, Duration.QUARTER, 52), + (None, Duration.QUARTER, 0), + (None, Duration.WHOLE, 0), +] +for note, dur, vel in glock_hits: + if note is None: + glock.rest(dur) + else: + glock.add(note, dur, velocity=vel) + +# Bars 41-56: more active — 16th note runs +glock_run = [G, A, B, D, B, A, G, Fs, G, B, D, E, D, B, A, G] +for _ in range(4): + for note in glock_run: + glock.add(note, Duration.SIXTEENTH, velocity=55) +for _ in range(4): + for note, dur, vel in glock_hits: + if note is None: + glock.rest(dur) + else: + glock.add(note, dur, velocity=vel) +for _ in range(4): + for note in glock_run: + glock.add(note, Duration.SIXTEENTH, velocity=58) + +# Bars 57-72: fading runs +for _ in range(4): + for note in glock_run: + glock.add(note, Duration.SIXTEENTH, velocity=max(20, 48)) +for _ in range(12): + glock.rest(Duration.WHOLE) + +# ── XYLOPHONE — bright wood, enters bar 49 ─────────────────── +xylo = score.part("xylophone", instrument="xylophone", volume=0.25, + reverb=0.15, reverb_decay=0.6, + delay=0.1, delay_time=0.139, delay_feedback=0.12, + pan=0.35, humanize=0.06) + +for _ in range(48): + xylo.rest(Duration.WHOLE) + +# Bars 49-56: rapid arps — woody brightness +xylo_arp_a = [G, B, D, G, D, B, G, D] +xylo_arp_b = [A, C, E, A, E, C, A, E] +for _ in range(4): + for note in xylo_arp_a: + xylo.add(note, Duration.SIXTEENTH, velocity=65) + for note in xylo_arp_b: + xylo.add(note, Duration.SIXTEENTH, velocity=62) + +# Bars 57-64: continues under kalimba melody +for _ in range(4): + for note in xylo_arp_a: + xylo.add(note, Duration.SIXTEENTH, velocity=58) + for note in xylo_arp_b: + xylo.add(note, Duration.SIXTEENTH, velocity=55) + +# Bars 65-72: fading +for vel in [50, 42, 35, 28, 22, 15, 0, 0]: + if vel > 0: + for note in xylo_arp_a: + xylo.add(note, Duration.SIXTEENTH, velocity=vel) + for note in xylo_arp_b: + xylo.add(note, Duration.SIXTEENTH, velocity=max(15, vel - 5)) + else: + xylo.rest(Duration.WHOLE) + +# ── CROTALES — crystalline, enters bar 49 ───────────────────── +crot = score.part("crotales", instrument="crotales", volume=0.18, + reverb=0.35, reverb_type="taj_mahal", + delay=0.15, delay_time=0.556, delay_feedback=0.2, + pan=-0.35) + +for _ in range(48): + crot.rest(Duration.WHOLE) + +# Bars 49-56: sparse strikes — like tiny church bells +crot_map = {49: (D, 52), 51: (G, 48), 53: (B, 50), 55: (E, 45)} +for bar in range(49, 73): + if bar in crot_map: + note, vel = crot_map[bar] + crot.add(note, Duration.WHOLE, velocity=vel) + elif bar > 56 and bar % 3 == 0: + crot.add(D, Duration.WHOLE, velocity=max(20, 45 - (bar - 57) * 2)) + else: + crot.rest(Duration.WHOLE) + +# ── TUBULAR BELLS — section markers ─────────────────────────── +bells = score.part("tubular_bells", instrument="tubular_bells", volume=0.2, + reverb=0.4, reverb_type="cathedral", + delay=0.15, delay_time=0.556, delay_feedback=0.15, + pan=0.15) + +bell_bars = {1: 60, 9: 55, 17: 52, 25: 58, 33: 55, 41: 62, 49: 58, 57: 65} +for bar in range(1, 73): + if bar in bell_bars: + bells.add(G.add(-12), Duration.WHOLE, velocity=bell_bars[bar]) + else: + bells.rest(Duration.WHOLE) + +# ── TIMPANI — the low end, enters bar 25 ────────────────────── +timp = score.part("timpani", instrument="timpani", volume=0.3, + reverb=0.2, reverb_decay=1.0, + delay=0.06, delay_time=0.278, delay_feedback=0.08, + pan=0.05, humanize=0.06) + +for _ in range(24): + timp.rest(Duration.WHOLE) + +# Bars 25-32: sparse — one hit per bar, like a grandfather clock +for note, vel in [(G.add(-12), 68), (D.add(-12), 62), + (G.add(-12), 65), (None, 0), + (G.add(-12), 70), (B.add(-12), 60), + (G.add(-12), 68), (D.add(-12), 62)]: + if note is None: + timp.rest(Duration.WHOLE) + else: + timp.add(note, Duration.QUARTER, velocity=vel) + timp.rest(Duration.DOTTED_HALF) + +# Bars 33-48: more active — rhythmic pulse +for _ in range(16): + timp.add(G.add(-12), Duration.QUARTER, velocity=65) + timp.rest(Duration.QUARTER) + timp.add(D.add(-12), Duration.QUARTER, velocity=58) + timp.rest(Duration.QUARTER) + +# Bars 49-56: peak — 8th note rolls +for bar in range(8): + if bar % 4 == 3: + for i in range(16): + timp.add(G.add(-12), Duration.SIXTEENTH, velocity=min(82, 48 + i * 2)) + else: + timp.add(G.add(-12), Duration.QUARTER, velocity=68) + timp.rest(Duration.QUARTER) + timp.add(D.add(-12), Duration.QUARTER, velocity=60) + timp.rest(Duration.QUARTER) + +# Bars 57-72: fading +for vel in [58, 52, 45, 38, 32, 25, 18, 10, 0, 0, 0, 0, 0, 0, 0, 0]: + if vel > 0: + timp.add(G.add(-12), Duration.QUARTER, velocity=vel) + timp.rest(Duration.DOTTED_HALF) + else: + timp.rest(Duration.WHOLE) + +# ═════════════════════════════════════════════════════════════════ +import sys + +print(f"Key: {key}") +print(f"BPM: 108") +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 MUSIC BOX FACTORY (live engine)...") + from pytheory_live.live import LiveEngine + engine = LiveEngine(buffer_size=1024) + engine.play_score(score) +else: + print("Playing MUSIC BOX FACTORY...") + play_score(score)