Files
Richard ZinkandClaude Opus 5.5 9800ce7156 Holiday music: a dark carol for Christmas, a spring tune for Easter, a harpsichord dance for Halloween
During a holiday the title music plays its piece, the build music every fourth time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 00:12:08 +01:00

1593 lines
69 KiB
Python
Executable File

#!/usr/bin/env python3
"""DungeonCraft soundtrack composer.
Writes FastTracker 2 / MilkyTracker compatible XM modules to assets/music/.
All instruments are synthesised chip waveforms (pulse, square, triangle, detuned saw pad)
and synthesised drums. The title leitmotif appears in every piece:
Dm | Gm A | Dm | F
D A F | G F E C#| D F A D | C A
Bb | Dm | A | Dm
Bb A G | F E D F | E C# A C# | D
Usage: python3 tools/compose_music.py (writes assets/music/*.xm)
The generated files can be opened and edited in MilkyTracker.
"""
import math
import os
import random
import struct
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "assets", "music")
ROWS_PER_BEAT = 4
BAR = 16 # rows per 4/4 bar
# ------------------------------------------------------------------ notes
NOTE_BASE = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11}
KEY_OFF = 97
def note_num(s):
"""'C#4', 'Bb3', 'D-4' -> XM note number (C-0 = 1)."""
n = NOTE_BASE[s[0].upper()]
i = 1
if s[i] == "#":
n += 1
i += 1
elif s[i] == "b":
n -= 1
i += 1
elif s[i] == "-":
i += 1
octave = int(s[i:])
return octave * 12 + n + 1
def transpose(seq, semis):
"""Transpose a melody string by semitones."""
out = []
names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
for tok in seq.split():
if tok == "|":
out.append(tok)
continue
n, l = tok.split(":")
if n in ("-", "="):
out.append(tok)
continue
v = note_num(n) - 1 + semis
out.append("%s%d:%s" % (names[v % 12], v // 12, l))
return " ".join(out)
# chord tones (pitch classes as note names, root first)
CHORDS = {
"Dm": ["D", "F", "A"],
"D": ["D", "F#", "A"],
"Gm": ["G", "Bb", "D"],
"G": ["G", "B", "D"],
"A": ["A", "C#", "E"],
"F": ["F", "A", "C"],
"Bb": ["Bb", "D", "F"],
"C": ["C", "E", "G"],
"Am": ["A", "C", "E"],
"C7": ["C", "E", "Bb"],
"E": ["E", "G#", "B"],
"Em": ["E", "G", "B"],
"Bm": ["B", "D", "F#"],
"Eb": ["Eb", "G", "Bb"],
}
def chord_notes(name, octave):
"""Chord tones from the given octave upwards."""
out = []
last = -1
for pc in CHORDS[name]:
v = note_num(pc + str(octave))
while v <= last:
v += 12
out.append(v)
last = v
return out
# ------------------------------------------------------------------ samples & instruments
C4_RATE = 8363.0
class Sample:
def __init__(self, name, data, loop=None, ping=False, volume=64, finetune=0, relnote=0, pan=128):
self.name = name
self.data = data # floats -1..1
self.loop = loop # (start, length) or None
self.ping = ping
self.volume = volume
self.finetune = finetune
self.relnote = relnote
self.pan = pan
class Instrument:
def __init__(self, name, sample, env=None, sustain=None, loop=None, fadeout=0, vib=(0, 0, 0, 0)):
self.name = name
self.sample = sample
self.env = env # list of (tick, 0..64)
self.sustain = sustain
self.loop = loop # (start point, end point)
self.fadeout = fadeout
self.vib = vib # (type, sweep, depth, rate)
CYCLE = 128 # samples per single cycle -> C2 at 8363 Hz; relnote 24 maps note C-4 to C4
CYCLE_REL = 24
CYCLE_FT = 2 # 8363/128 = 65.34 Hz vs. C2 = 65.41 Hz -> +2/128 semitone
def pulse(duty, amp=0.42):
hi = int(CYCLE * duty)
# remove DC so the waveform is centred
low = -amp * duty / (1.0 - duty)
return [amp if i < hi else low for i in range(CYCLE)]
def triangle(amp=0.55):
return [amp * (4.0 * abs(i / CYCLE - 0.5) - 1.0) for i in range(CYCLE)]
def saw_pad(amp=0.32):
# two saws with 128- and 127-sample cycles: loops seamlessly after 128*127 samples, natural chorus
n = 128 * 127
return [amp * (((i % 128) / 64.0 - 1.0) + ((i % 127) / 63.5 - 1.0)) * 0.5 for i in range(n)]
DRUM_RATE = C4_RATE * 2 # drums are rendered at 16726 Hz and played at C-4 with relnote +12
def kick():
n = int(DRUM_RATE * 0.32)
out, ph = [], 0.0
for i in range(n):
t = i / DRUM_RATE
f = 45 + 110 * math.exp(-t * 28)
ph += 2 * math.pi * f / DRUM_RATE
out.append(0.95 * math.sin(ph) * math.exp(-t * 9))
return out
def snare():
rnd = random.Random(7)
n = int(DRUM_RATE * 0.22)
out, ph = [], 0.0
for i in range(n):
t = i / DRUM_RATE
ph += 2 * math.pi * 185 / DRUM_RATE
tone = 0.45 * math.sin(ph) * math.exp(-t * 30)
noise = 0.6 * (rnd.random() * 2 - 1) * math.exp(-t * 16)
out.append(max(-1, min(1, tone + noise)))
return out
def hihat():
rnd = random.Random(3)
n = int(DRUM_RATE * 0.07)
out, prev = [], 0.0
for i in range(n):
t = i / DRUM_RATE
x = rnd.random() * 2 - 1
out.append(0.6 * (x - prev) * 0.5 * math.exp(-t * 60))
prev = x
return out
# ---- additional timbres (used by the generated pieces)
def organ(amp=0.42):
harm = [(1, 1.0), (2, 0.6), (3, 0.4), (4, 0.3), (6, 0.2), (8, 0.15)]
raw = [sum(a * math.sin(2 * math.pi * h * i / CYCLE) for h, a in harm) for i in range(CYCLE)]
m = max(abs(v) for v in raw)
return [amp * v / m for v in raw]
def sine_cycle(amp=0.6):
return [amp * math.sin(2 * math.pi * i / CYCLE) for i in range(CYCLE)]
def harp_cycle(amp=0.5):
raw = [math.sin(2 * math.pi * i / CYCLE) + 0.35 * math.sin(4 * math.pi * i / CYCLE) + 0.15 * math.sin(6 * math.pi * i / CYCLE) for i in range(CYCLE)]
m = max(abs(v) for v in raw)
return [amp * v / m for v in raw]
def choir_pad(amp=0.36):
# vowel-like spectrum ("ah": strong 2nd/3rd and 7th/8th harmonics), two detuned copies -> chorus
harm = [(1, 0.5), (2, 1.0), (3, 0.8), (4, 0.3), (5, 0.2), (7, 0.45), (8, 0.4), (9, 0.15)]
def wave(ph):
return sum(a * math.sin(2 * math.pi * h * ph) for h, a in harm)
n = 128 * 127
raw = [wave((i % 128) / 128.0) + wave((i % 127) / 127.0) for i in range(n)]
m = max(abs(v) for v in raw)
return [amp * v / m for v in raw]
def bell():
# inharmonic partials, rendered at C4 for 16726 Hz playback (relnote +12)
f0 = 261.63
n = int(DRUM_RATE * 1.3)
parts = [(1.0, 1.0, 2.5), (2.0, 0.5, 3.5), (2.76, 0.4, 5.0), (5.4, 0.25, 8.0), (8.9, 0.12, 12.0)]
out = []
for i in range(n):
t = i / DRUM_RATE
out.append(0.5 * sum(a * math.exp(-t * d) * math.sin(2 * math.pi * f0 * r * t) for r, a, d in parts))
return out
def tom():
n = int(DRUM_RATE * 0.35)
out, ph = [], 0.0
for i in range(n):
t = i / DRUM_RATE
f = 95 + 90 * math.exp(-t * 18)
ph += 2 * math.pi * f / DRUM_RATE
out.append(0.85 * math.sin(ph) * math.exp(-t * 9))
return out
def crash():
rnd = random.Random(11)
n = int(DRUM_RATE * 1.0)
out, prev = [], 0.0
for i in range(n):
t = i / DRUM_RATE
x = rnd.random() * 2 - 1
out.append(0.55 * (x - 0.7 * prev) * math.exp(-t * 3.5))
prev = x
return out
def timpani():
rnd = random.Random(5)
n = int(DRUM_RATE * 0.9)
out, ph = [], 0.0
for i in range(n):
t = i / DRUM_RATE
f = 98 - 8 * math.exp(-t * 6)
ph += 2 * math.pi * f / DRUM_RATE
hit = 0.3 * (rnd.random() * 2 - 1) * math.exp(-t * 40)
out.append(0.8 * math.sin(ph) * math.exp(-t * 3.2) + hit)
return out
def flute_cycle(amp=0.5):
# soft and hollow: a sine with a little triangle and a quiet second harmonic
raw = [math.sin(2 * math.pi * i / CYCLE) + 0.18 * (4.0 * abs(i / CYCLE - 0.5) - 1.0) + 0.12 * math.sin(4 * math.pi * i / CYCLE) for i in range(CYCLE)]
m = max(abs(v) for v in raw)
return [amp * v / m for v in raw]
def bird():
"""A little songbird: three quick rising chirps and a falling trill (rendered for C-4)."""
out = []
ph = 0.0
phrases = [(2600, 3900, 0.06), (2700, 4100, 0.06), (2800, 4300, 0.05), (4200, 3000, 0.11)]
for k, (f0, f1, dur) in enumerate(phrases):
n = int(DRUM_RATE * dur)
for i in range(n):
t = i / n
f = f0 + (f1 - f0) * t
# a fast warble on the last one
if k == 3:
f += 250 * math.sin(2 * math.pi * 28 * i / DRUM_RATE)
ph += 2 * math.pi * f / DRUM_RATE
env = math.sin(math.pi * t) ** 0.6
out.append(0.55 * env * math.sin(ph))
out += [0.0] * int(DRUM_RATE * 0.035)
return out
def wind():
"""Soft wind: filtered noise that swells and fades (a seamless loop of two seconds)."""
rnd = random.Random(23)
n = int(DRUM_RATE * 2.0)
out, lp, lp2 = [], 0.0, 0.0
for i in range(n):
x = rnd.random() * 2 - 1
lp += 0.06 * (x - lp)
lp2 += 0.06 * (lp - lp2)
swell = 0.6 + 0.4 * math.sin(2 * math.pi * i / n)
out.append(2.2 * lp2 * swell)
# cross-fade the ends so the loop has no click
k = int(DRUM_RATE * 0.1)
for i in range(k):
a = i / k
out[i] = out[i] * a + out[n - k + i] * (1 - a)
return out[: n - k]
def harpsichord_cycle(amp=0.4):
# bright and nasal: a narrow pulse with a saw on top (many strong harmonics, like a plucked quill)
raw = [(1.0 if i < CYCLE * 0.18 else -0.22) + 0.6 * (2.0 * i / CYCLE - 1.0) for i in range(CYCLE)]
mean = sum(raw) / CYCLE
raw = [v - mean for v in raw]
m = max(abs(v) for v in raw)
return [amp * v / m for v in raw]
def build_instruments():
return [
Instrument("Lead pulse 25%", Sample("pulse25", pulse(0.25), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 0), (2, 64), (12, 46), (40, 40)], sustain=3, fadeout=1400, vib=(0, 24, 3, 10)),
Instrument("Square", Sample("square", pulse(0.5, 0.36), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 0), (2, 56), (16, 40), (50, 34)], sustain=3, fadeout=1600, vib=(0, 30, 2, 9)),
Instrument("Saw pad", Sample("saw pad", saw_pad(), loop=(0, 128 * 127), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 0), (30, 52), (60, 48)], sustain=2, fadeout=500),
Instrument("Triangle bass", Sample("triangle", triangle(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 64), (6, 56), (40, 44)], sustain=2, fadeout=2800),
Instrument("Pluck 12%", Sample("pulse12", pulse(0.125, 0.38), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 64), (5, 32), (24, 8), (48, 0)], fadeout=3000),
Instrument("Kick", Sample("kick", kick(), relnote=12)),
Instrument("Snare", Sample("snare", snare(), relnote=12)),
Instrument("Hihat", Sample("hihat", hihat(), relnote=12)),
Instrument("Organ", Sample("organ", organ(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 0), (2, 60), (40, 56)], sustain=2, fadeout=900),
Instrument("Bell", Sample("bell", bell(), relnote=12)),
Instrument("Choir", Sample("choir", choir_pad(), loop=(0, 128 * 127), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 0), (40, 50), (80, 46)], sustain=2, fadeout=450),
Instrument("Sub bass", Sample("sine", sine_cycle(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 64), (10, 54), (60, 48)], sustain=2, fadeout=2400),
Instrument("Tom", Sample("tom", tom(), relnote=12)),
Instrument("Crash", Sample("crash", crash(), relnote=12)),
Instrument("Harp", Sample("harp", harp_cycle(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 64), (4, 44), (30, 12), (70, 0)], fadeout=2000),
Instrument("Timpani", Sample("timpani", timpani(), relnote=12)),
Instrument("Flute", Sample("flute", flute_cycle(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 0), (5, 58), (20, 48), (60, 44)], sustain=3, fadeout=1100, vib=(0, 40, 4, 11)),
Instrument("Bird", Sample("bird", bird(), relnote=12)),
Instrument("Wind", Sample("wind", wind(), loop=(0, len(wind())), relnote=12),
env=[(0, 0), (60, 48), (120, 40)], sustain=2, fadeout=200),
Instrument("Harpsichord", Sample("harpsichord", harpsichord_cycle(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
env=[(0, 64), (2, 46), (14, 22), (40, 8), (70, 0)], fadeout=2400),
]
LEAD, SQUARE, PAD, BASS, PLUCK, KICK, SNARE, HAT = range(1, 9)
ORGAN, BELL, CHOIR, SUB, TOM, CRASH, HARP, TIMP = range(9, 17)
FLUTE, BIRD, WIND, HARPSI = range(17, 21)
# channel layout (the last two only for songs above ground: birds and wind)
CH_LEAD, CH_COUNTER, CH_PAD1, CH_PAD2, CH_BASS, CH_ARP, CH_DRUM, CH_HAT, CH_BIRD, CH_WIND = range(10)
CHANNELS = 10
BASE_CHANNELS = 8 # songs that leave birds and wind out are written with 8 channels
# ------------------------------------------------------------------ patterns
class Pattern:
def __init__(self, bars=4):
self.rows = bars * BAR
self.cells = [[None] * CHANNELS for _ in range(self.rows)]
def set(self, row, ch, note=0, inst=0, vol=None, fx=0, param=0):
if 0 <= row < self.rows:
v = 0 if vol is None else 0x10 + max(0, min(64, vol))
self.cells[row][ch] = (note, inst, v, fx, param)
def line(self, ch, seq, inst, vol=None, start=0, release=True):
"""Melody from 'D4:8 A4:4 F4:4 | -:4 ...' (lengths in rows, '-' rest, '=' key off)."""
r = start
for tok in seq.split():
if tok == "|":
continue
n, l = tok.split(":")
l = int(l)
if n == "-":
if release:
self.set(r, ch, KEY_OFF)
elif n == "=":
self.set(r, ch, KEY_OFF)
else:
self.set(r, ch, note_num(n), inst, vol)
r += l
return r
def bass(self, chords, pattern, inst=BASS, vol=48, octave=2, start=0):
"""chords: list of (chord, rows); pattern: per-bar string of 16 chars:
'R' root, 'F' fifth, 'O' octave, 'T' third, '.' hold, '-' release."""
r = start
for name, rows in chords:
notes = chord_notes(name, octave)
root, third, fifth = notes[0], notes[1], notes[2]
for k in range(rows):
c = pattern[(r + k) % len(pattern)]
n = {"R": root, "F": fifth, "O": root + 12, "T": third}.get(c)
if n:
self.set(r + k, CH_BASS, n, inst, vol)
elif c == "-":
self.set(r + k, CH_BASS, KEY_OFF)
r += rows
def pads(self, chords, vol=22, octave=4, start=0):
r = start
for name, rows in chords:
notes = chord_notes(name, octave)
self.set(r, CH_PAD1, notes[1], PAD, vol)
self.set(r, CH_PAD2, notes[2], PAD, vol)
r += rows
def arp(self, chords, step=2, vol=18, octave=5, start=0, order=(0, 1, 2, 1), ch=CH_ARP, inst=PLUCK):
r = start
for name, rows in chords:
notes = chord_notes(name, octave)
k = 0
for rr in range(0, rows, step):
self.set(r + rr, ch, notes[order[k % len(order)]], inst, vol)
k += 1
r += rows
def drums(self, kick="", snare="", hat="", kv=60, sv=44, hv=18):
"""16-char strings per bar, 'x' = hit (hat: 'x' normal, 'o' accent)."""
for bar in range(self.rows // BAR):
for i in range(BAR):
r = bar * BAR + i
if kick and kick[i] == "x":
self.set(r, CH_DRUM, note_num("C4"), KICK, kv)
elif snare and snare[i] == "x":
self.set(r, CH_DRUM, note_num("C4"), SNARE, sv)
if hat and hat[i] in "xo":
self.set(r, CH_HAT, note_num("C4"), HAT, hv + (10 if hat[i] == "o" else 0))
def echo(self, src, dst, delay=3, vol=22, inst=SQUARE):
"""Classic tracker echo: copy notes of one channel delayed and quieter."""
for r in range(self.rows):
c = self.cells[r][src]
if c and 0 < c[0] < KEY_OFF:
self.set(r + delay, dst, c[0], inst, vol)
def bars(*items):
"""('Dm', 1), ('Gm', .5) ... -> list of (chord, rows)"""
return [(c, int(b * BAR)) for c, b in items]
# ------------------------------------------------------------------ the leitmotif
MOTIF_A = "D4:8 A4:4 F4:4 | G4:6 F4:2 E4:4 C#4:4 | D4:4 F4:4 A4:4 D5:4 | C5:8 A4:8"
MOTIF_B = "Bb4:8 A4:4 G4:4 | F4:6 E4:2 D4:4 F4:4 | E4:4 C#4:4 A3:4 C#4:4 | D4:16"
CHORDS_A = bars(("Dm", 1), ("Gm", .5), ("A", .5), ("Dm", 1), ("F", 1))
CHORDS_B = bars(("Bb", 1), ("Dm", 1), ("A", 1), ("Dm", 1))
def double(seq):
"""Augmentation: every length doubled."""
return " ".join(t if t == "|" else "%s:%d" % (t.split(":")[0], int(t.split(":")[1]) * 2) for t in seq.split())
def half(seq):
"""Diminution: every length halved."""
return " ".join(t if t == "|" else "%s:%d" % (t.split(":")[0], max(1, int(t.split(":")[1]) // 2)) for t in seq.split())
# ------------------------------------------------------------------ songs
class Song:
def __init__(self, name, bpm, speed=6):
self.name = name
self.bpm = bpm
self.speed = speed
self.patterns = []
self.order = []
self.restart = 0
def add(self, pat):
self.patterns.append(pat)
return len(self.patterns) - 1
def song_title():
s = Song("DungeonCraft - Title", 108)
intro = Pattern()
ch = bars(("Dm", 1), ("Bb", 1), ("Gm", 1), ("A", 1))
intro.pads(ch)
intro.bass(ch, "R.......R...F...")
intro.arp(ch, vol=14)
intro.drums(hat="x...x...x...x...", hv=12)
a = Pattern()
a.line(CH_LEAD, MOTIF_A, LEAD, 46)
a.pads(CHORDS_A)
a.bass(CHORDS_A, "R...R.F.R...F.O.")
a.arp(CHORDS_A, vol=14)
a.drums(kick="x.......x.......", snare="....x.......x...", hat="x.x.x.x.x.x.x.x.", sv=36, hv=12)
b = Pattern()
b.line(CH_LEAD, MOTIF_B, LEAD, 46)
b.pads(CHORDS_B)
b.bass(CHORDS_B, "R...R.F.R...F.O.")
b.arp(CHORDS_B, vol=14)
b.drums(kick="x.......x.......", snare="....x.......x...", hat="x.x.x.x.x.x.x.x.", sv=36, hv=12)
# second statement: lead an octave higher, counter melody a third below + echo
a2 = Pattern()
a2.line(CH_LEAD, transpose(MOTIF_A, 12), LEAD, 44)
a2.line(CH_COUNTER, "F4:8 F4:4 D4:4 | Bb3:6 A3:2 C#4:4 A3:4 | F3:4 A3:4 D4:4 F4:4 | A4:8 F4:8", SQUARE, 30)
a2.pads(CHORDS_A)
a2.bass(CHORDS_A, "R.R.R.F.R.R.F.O.")
a2.arp(CHORDS_A, vol=16, octave=5, order=(0, 1, 2, 3 % 3))
a2.drums(kick="x.....x.x.......", snare="....x.......x..x", hat="x.x.x.x.x.x.x.x.", sv=40, hv=14)
b2 = Pattern()
b2.line(CH_LEAD, transpose(MOTIF_B, 12), LEAD, 44)
b2.line(CH_COUNTER, "D4:8 F4:4 E4:4 | D4:6 C#4:2 A3:4 D4:4 | C#4:4 A3:4 E3:4 A3:4 | F3:16", SQUARE, 30)
b2.pads(CHORDS_B)
b2.bass(CHORDS_B, "R.R.R.F.R.R.F.O.")
b2.arp(CHORDS_B, vol=16)
b2.drums(kick="x.....x.x.......", snare="....x.......x..x", hat="x.x.x.x.x.x.x.x.", sv=40, hv=14)
bridge = Pattern()
bch = bars(("Bb", 1), ("C", 1), ("Dm", 1), ("A", 1))
bridge.line(CH_LEAD, "Bb4:8 F5:4 D5:4 | C5:8 G4:4 E4:4 | D5:4 C5:4 A4:4 F4:4 | E4:8 C#4:8", LEAD, 44)
bridge.echo(CH_LEAD, CH_COUNTER, 3, 22)
bridge.pads(bch)
bridge.bass(bch, "R...R...R...F...")
bridge.arp(bch, vol=14)
bridge.drums(kick="x...............", snare="............x...", hat="x...x...x...x...", hv=12)
p = [s.add(x) for x in (intro, a, b, a2, b2, bridge)]
s.order = [p[0], p[1], p[2], p[3], p[4], p[5], p[1], p[2]]
s.restart = 1
return s
def song_build1():
"""Deep Halls: slow, the motif augmented over its own harmony."""
s = Song("DungeonCraft - Deep Halls", 84)
ch1 = bars(("Dm", 1), ("Dm", 1), ("Gm", 1), ("A", 1))
ch2 = bars(("Dm", 1), ("Dm", 1), ("F", 1), ("F", 1))
ch3 = bars(("Bb", 1), ("Gm", 1), ("A", 1), ("A", 1))
p0 = Pattern()
p0.pads(ch1, vol=20)
p0.bass(ch1, "R...............", vol=40)
p1 = Pattern()
p1.pads(ch2, vol=20)
p1.bass(ch2, "R.......F.......", vol=40)
p1.arp(ch2, step=4, vol=12, octave=5)
aug = double(MOTIF_A).split("|")
p2 = Pattern()
p2.line(CH_LEAD, "|".join(aug[0:2]), SQUARE, 30)
p2.echo(CH_LEAD, CH_COUNTER, 4, 16)
p2.pads(ch1, vol=20)
p2.bass(ch1, "R.......F.......", vol=40)
p2.arp(ch1, step=4, vol=12)
p3 = Pattern()
p3.line(CH_LEAD, "|".join(aug[2:4]), SQUARE, 30)
p3.echo(CH_LEAD, CH_COUNTER, 4, 16)
p3.pads(ch2, vol=20)
p3.bass(ch2, "R.......F.......", vol=40)
p3.arp(ch2, step=4, vol=12)
p4 = Pattern()
p4.pads(ch3, vol=20)
p4.bass(ch3, "R.......R...F...", vol=40)
p4.arp(ch3, step=2, vol=11, order=(0, 2, 1, 2))
p4.line(CH_LEAD, "-:48 D5:4 C#5:4 A4:8", PLUCK, 26)
p = [s.add(x) for x in (p0, p1, p2, p3, p4)]
s.order = [p[0], p[1], p[2], p[3], p[4], p[1], p[2], p[3], p[4]]
s.restart = 1
return s
def song_build2():
"""Imp Work: bouncy D dorian, the motif's opening as a call."""
s = Song("DungeonCraft - Imp Work", 100)
ch = bars(("Dm", 1), ("G", 1), ("Dm", 1), ("C", 1))
groove = "R.-.R.F.-.R.O.F."
p0 = Pattern()
p0.bass(ch, groove, vol=44)
p0.drums(kick="x.......x.......", hat="..x...x...x...x.", hv=14)
p0.arp(ch, step=4, vol=12, order=(0, 2))
p1 = Pattern()
p1.line(CH_LEAD, "D5:4 A4:4 F4:4 A4:4 | G4:6 F4:2 E4:4 D4:4 | F4:4 A4:4 C5:4 A4:4 | G4:8 E4:8", SQUARE, 36)
p1.pads(ch, vol=16)
p1.bass(ch, groove, vol=44)
p1.drums(kick="x.......x.......", snare="............x...", hat="..x...x...x...x.", sv=30, hv=14)
p2 = Pattern()
p2.line(CH_LEAD, "D5:4 A4:4 F4:4 A4:4 | B4:6 A4:2 G4:4 B4:4 | A4:4 F4:4 D4:4 F4:4 | E4:8 C4:4 E4:4", SQUARE, 36)
p2.echo(CH_LEAD, CH_COUNTER, 3, 18)
p2.pads(ch, vol=16)
p2.bass(ch, groove, vol=44)
p2.drums(kick="x.......x.......", snare="............x...", hat="..x...x...x...x.", sv=30, hv=14)
p3 = Pattern()
# call: the motif's first bar, quick, answered by the pluck
p3.line(CH_LEAD, "D5:2 A5:2 F5:4 -:8 | -:16 | D5:2 A5:2 F5:4 -:4 G5:2 E5:2 | D5:16", PLUCK, 34)
p3.line(CH_COUNTER, "-:8 A4:2 F4:2 D4:4 | -:16 | -:16 | -:16", PLUCK, 22)
p3.pads(ch, vol=16)
p3.bass(ch, groove, vol=44)
p3.arp(ch, step=2, vol=12)
p3.drums(kick="x.......x.......", snare="............x...", hat="..x...x...x...x.", sv=30, hv=14)
p = [s.add(x) for x in (p0, p1, p2, p3)]
s.order = [p[0], p[1], p[2], p[3], p[1], p[2], p[3]]
s.restart = 1
return s
def song_build3():
"""Torchlight: the motif in F major, warm, with echo."""
s = Song("DungeonCraft - Torchlight", 92)
motif_f_a = "F4:8 C5:4 A4:4 | Bb4:6 A4:2 G4:4 E4:4 | F4:4 A4:4 C5:4 F5:4 | E5:8 C5:8"
motif_f_b = "D5:8 C5:4 Bb4:4 | A4:6 G4:2 F4:4 A4:4 | G4:4 E4:4 C4:4 E4:4 | F4:16"
cha = bars(("F", 1), ("C7", 1), ("F", 1), ("C", 1))
chb = bars(("Bb", 1), ("F", 1), ("C", 1), ("F", 1))
p0 = Pattern()
p0.pads(cha, vol=18)
p0.bass(cha, "R.......F.......", vol=40)
p0.arp(cha, step=2, vol=12, order=(0, 1, 2, 1))
p1 = Pattern()
p1.line(CH_LEAD, motif_f_a, LEAD, 36)
p1.echo(CH_LEAD, CH_COUNTER, 3, 18)
p1.pads(cha, vol=18)
p1.bass(cha, "R.......F.......", vol=40)
p1.arp(cha, step=2, vol=11)
p1.drums(hat="x...x...x...x...", hv=10)
p2 = Pattern()
p2.line(CH_LEAD, motif_f_b, LEAD, 36)
p2.echo(CH_LEAD, CH_COUNTER, 3, 18)
p2.pads(chb, vol=18)
p2.bass(chb, "R.......F.......", vol=40)
p2.arp(chb, step=2, vol=11)
p2.drums(hat="x...x...x...x...", hv=10)
p3 = Pattern()
chc = bars(("Dm", 1), ("Bb", 1), ("C", 1), ("C", 1))
p3.pads(chc, vol=18)
p3.bass(chc, "R...R...F...R...", vol=40)
p3.arp(chc, step=1, vol=10, order=(0, 1, 2, 3 % 3, 2, 1))
p3.line(CH_LEAD, "A4:16 | Bb4:16 | C5:8 E5:8 | G5:16", SQUARE, 24)
p = [s.add(x) for x in (p0, p1, p2, p3)]
s.order = [p[0], p[1], p[2], p[3], p[1], p[2], p[3]]
s.restart = 1
return s
def song_battle1():
"""Heroes at the Gate: driving minor, full motif plus riff."""
s = Song("DungeonCraft - Heroes at the Gate", 140)
drive = "RRRRRRRRRRRRRRRR"
eighths = "R.R.R.R.R.R.R.R."
rock = dict(kick="x.....x.x.......", snare="....x.......x...", hat="x.x.x.x.x.x.x.x.")
intro = Pattern()
ich = bars(("Dm", 1), ("Dm", 1), ("Bb", 1), ("A", 1))
intro.bass(ich, eighths, vol=48)
intro.drums(kick="x...x...x...x...", hat="x.x.x.x.x.x.x.x.", hv=16)
intro.line(CH_LEAD, "-:48 A4:4 C#5:4 E5:4 A5:4", SQUARE, 34)
a = Pattern()
a.line(CH_LEAD, transpose(MOTIF_A, 12), LEAD, 46)
a.pads(CHORDS_A, vol=18)
a.bass(CHORDS_A, eighths, vol=48)
a.arp(CHORDS_A, step=1, vol=10, order=(0, 1, 2))
a.drums(**rock, sv=46, hv=16)
b = Pattern()
b.line(CH_LEAD, transpose(MOTIF_B, 12), LEAD, 46)
b.pads(CHORDS_B, vol=18)
b.bass(CHORDS_B, eighths, vol=48)
b.arp(CHORDS_B, step=1, vol=10, order=(0, 1, 2))
b.drums(**rock, sv=46, hv=16)
riff_ch = bars(("Dm", 1), ("C", 1), ("Bb", 1), ("A", 1))
riff = "D5:2 -:2 D5:2 A5:2 F5:4 D5:4 | C5:2 -:2 C5:2 G5:2 E5:4 C5:4 | Bb4:2 -:2 Bb4:2 F5:2 D5:4 Bb4:4 | A4:4 C#5:4 E5:4 A5:4"
r1 = Pattern()
r1.line(CH_LEAD, riff, SQUARE, 40)
r1.pads(riff_ch, vol=16)
r1.bass(riff_ch, drive, vol=44)
r1.drums(kick="x.x...x.x.x...x.", snare="....x.......x...", hat="x.x.x.x.x.x.x.x.", sv=46, hv=16)
r2 = Pattern()
r2.line(CH_LEAD, riff, SQUARE, 40)
r2.line(CH_COUNTER, transpose(riff, -12), PLUCK, 28)
r2.pads(riff_ch, vol=16)
r2.bass(riff_ch, drive, vol=44)
r2.drums(kick="x.x...x.x.x...x.", snare="....x.......x.xx", hat="xxxxxxxxxxxxxxxx", sv=46, hv=12)
p = [s.add(x) for x in (intro, a, b, r1, r2)]
s.order = [p[0], p[1], p[2], p[3], p[4], p[1], p[2], p[3], p[4]]
s.restart = 1
return s
def song_battle2():
"""Storm the Gate: fast, the motif in diminution over pounding drums."""
s = Song("DungeonCraft - Storm the Gate", 156)
sixteen = "RRRRRRRRRRRRRRRR"
ch1 = bars(("Bb", 1), ("C", 1), ("Dm", 1), ("Dm", 1))
ch2 = bars(("Dm", .5), ("Gm", .25), ("A", .25), ("Dm", .5), ("F", .5), ("Bb", .5), ("Dm", .5), ("A", 1))
p0 = Pattern()
p0.bass(ch1, sixteen, vol=44)
p0.pads(ch1, vol=18)
p0.drums(kick="x...x...x...x...", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", sv=44, hv=10)
p0.line(CH_LEAD, "-:32 D5:4 E5:4 F5:4 G5:4 | A5:16", SQUARE, 34)
dim = half(MOTIF_A) + " | " + half(MOTIF_B)
p1 = Pattern()
p1.line(CH_LEAD, transpose(dim, 12), LEAD, 46)
p1.line(CH_COUNTER, dim, SQUARE, 26)
p1.pads(ch2, vol=18)
p1.bass(ch2, sixteen, vol=44)
p1.arp(ch2, step=1, vol=10, order=(0, 1, 2, 1))
p1.drums(kick="x.x.x...x.x.x...", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", sv=48, hv=10)
p2 = Pattern()
p2.line(CH_LEAD, "Bb5:4 A5:4 F5:4 D5:4 | C6:4 Bb5:4 G5:4 E5:4 | D6:4 C6:4 A5:4 F5:4 | D6:16", LEAD, 44)
p2.echo(CH_LEAD, CH_COUNTER, 2, 20)
p2.pads(ch1, vol=18)
p2.bass(ch1, sixteen, vol=44)
p2.arp(ch1, step=1, vol=10, order=(0, 1, 2))
p2.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x.xx", hat="xxxxxxxxxxxxxxxx", sv=48, hv=10)
p = [s.add(x) for x in (p0, p1, p2)]
s.order = [p[0], p[1], p[2], p[1], p[2]]
s.restart = 1
return s
def song_victory():
"""Victory jingle: the motif in D major; ends in a silent loop."""
s = Song("DungeonCraft - Victory", 120)
p0 = Pattern()
ch = bars(("D", 1), ("G", .5), ("A", .5), ("D", 2))
p0.line(CH_LEAD, "D5:4 A5:4 F#5:4 A5:4 | G5:6 F#5:2 E5:4 C#5:4 | D5:32", LEAD, 50)
p0.line(CH_COUNTER, "F#4:8 D4:8 | B4:8 A4:8 | F#4:32", SQUARE, 30)
p0.pads(ch, vol=24)
p0.bass(ch, "R...R...R...R...", vol=48)
p0.drums(kick="x.......x.......", snare="xxxx............", sv=30)
p0.set(63, CH_LEAD, KEY_OFF)
for c in range(BASE_CHANNELS):
p0.set(63, c, KEY_OFF)
silence = Pattern(1)
p = [s.add(x) for x in (p0, silence)]
s.order = [p[0], p[1]]
s.restart = 1
return s
def song_defeat():
"""Defeat jingle: the motif falls apart, slow and low; ends in a silent loop."""
s = Song("DungeonCraft - Defeat", 70)
p0 = Pattern()
ch = bars(("Dm", 1), ("Bb", 1), ("A", 1), ("Dm", 1))
p0.line(CH_LEAD, "A4:8 F4:8 | D4:8 Bb3:8 | A3:8 C#4:4 E4:4 | D4:16", LEAD, 44)
p0.pads(ch, vol=22, octave=3)
p0.bass(ch, "R...............", vol=44, octave=1)
p0.drums(kick="x...............", kv=44)
for c in range(BASE_CHANNELS):
p0.set(63, c, KEY_OFF)
silence = Pattern(1)
p = [s.add(x) for x in (p0, silence)]
s.order = [p[0], p[1]]
s.restart = 1
return s
# ------------------------------------------------------------------ XM writer
def write_xm(path, song, instruments):
"""Writes an XM file. Only instruments used by the song are stored (renumbered),
samples are 8-bit like on the Amiga to keep the files small."""
used = sorted({c[1] for pat in song.patterns for row in pat.cells for c in row if c and c[1]})
remap = {old: new + 1 for new, old in enumerate(used)}
out = bytearray()
out += b"Extended Module: "
out += song.name.encode("ascii")[:20].ljust(20, b" ")
out += b"\x1a"
out += b"DungeonCraft compose".ljust(20, b" ")
out += struct.pack("<H", 0x0104)
order = song.order + [0] * (256 - len(song.order))
nch = CHANNELS if any(row[c] for pat in song.patterns for row in pat.cells for c in range(BASE_CHANNELS, CHANNELS)) else BASE_CHANNELS
out += struct.pack("<IHHHHHHHH", 276, len(song.order), song.restart, nch, len(song.patterns),
len(used), 1, song.speed, song.bpm)
out += bytes(order)
for pat in song.patterns:
data = bytearray()
for row in pat.cells:
for c in row[:nch]:
if c is None:
data += b"\x80"
else:
note, inst, vol, fx, param = c
data += bytes([note, remap.get(inst, 0), vol, fx, param])
out += struct.pack("<IBHH", 9, 0, pat.rows, len(data))
out += data
for old in used:
ins = instruments[old - 1]
smp = ins.sample
hdr = bytearray()
hdr += ins.name.encode("ascii")[:22].ljust(22, b"\0")
hdr += b"\0" # type
hdr += struct.pack("<H", 1)
hdr += struct.pack("<I", 40)
hdr += bytes(96) # all notes -> sample 0
env = ins.env or []
pts = bytearray()
for (x, y) in env[:12]:
pts += struct.pack("<HH", x, y)
hdr += pts.ljust(48, b"\0")
hdr += bytes(48) # panning envelope
vol_type = 0
if env:
vol_type |= 1
if ins.sustain is not None:
vol_type |= 2
if ins.loop is not None:
vol_type |= 4
hdr += bytes([len(env), 0, ins.sustain or 0, (ins.loop or (0, 0))[0], (ins.loop or (0, 0))[1], 0, 0, 0, vol_type, 0])
hdr += bytes(ins.vib)
hdr += struct.pack("<H", ins.fadeout)
hdr += bytes(22) # reserved
out += struct.pack("<I", 4 + len(hdr))
out += hdr
n = len(smp.data)
loop_type = 0
ls, ll = 0, 0
if smp.loop:
loop_type = 2 if smp.ping else 1
ls, ll = smp.loop
out += struct.pack("<IIIBbBBbB", n, ls, ll, smp.volume, smp.finetune, loop_type, smp.pan, smp.relnote, 0)
out += smp.name.encode("ascii")[:22].ljust(22, b"\0")
prev = 0
for v in smp.data:
s8 = int(round(max(-1.0, min(1.0, v)) * 127))
out += struct.pack("<B", (s8 - prev) & 0xFF)
prev = s8
with open(path, "wb") as f:
f.write(out)
return len(out)
# ------------------------------------------------------------------ variant generator
#
# The leitmotif is stored as scale degrees of a minor scale (0 = tonic, 7 = octave; a degree
# marked "lt" is the raised leading tone). It can be realised in any key and mode and is
# transformed (augmented, diminished, inverted, fragmented, sequenced) for every piece.
MODES = {
"minor": [0, 2, 3, 5, 7, 8, 10],
"harmonic": [0, 2, 3, 5, 7, 8, 11],
"dorian": [0, 2, 3, 5, 7, 9, 10],
"phrygian": [0, 1, 3, 5, 7, 8, 10],
"major": [0, 2, 4, 5, 7, 9, 11],
"lydian": [0, 2, 4, 6, 7, 9, 11],
"mixolydian": [0, 2, 4, 5, 7, 9, 10],
}
# (degree, rows, leading tone?)
MOTIF_DEG_A = [(0, 8), (4, 4), (2, 4), (3, 6), (2, 2), (1, 4), (-1, 4, "lt"), (0, 4), (2, 4), (4, 4), (7, 4), (6, 8), (4, 8)]
MOTIF_DEG_B = [(5, 8), (4, 4), (3, 4), (2, 6), (1, 2), (0, 4), (2, 4), (1, 4), (-1, 4, "lt"), (-3, 4), (-1, 4, "lt"), (0, 16)]
# harmony of the motif as scale degrees per bar (pairs = half bars)
MOTIF_HARM_A = [[0], [3, 4], [0], [2]]
MOTIF_HARM_B = [[5], [0], [4], [0]]
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
def degree_semis(deg, mode, lt=False):
octv, d = divmod(deg, 7)
if lt:
return 12 * octv + 11
return 12 * octv + MODES[mode][d]
def name_of(midi):
return "%s%d" % (NOTE_NAMES[midi % 12], midi // 12)
def realize(motif, root, mode, octave):
"""motif -> 'X4:8 ...' melody string in the given key"""
base = 12 * octave + root
out = []
for item in motif:
deg, l = item[0], item[1]
lt = len(item) > 2 and mode in ("minor", "harmonic", "dorian", "phrygian")
out.append("%s:%d" % (name_of(base + degree_semis(deg, mode, lt)), l))
return " ".join(out)
def m_aug(m):
return [(i[0], i[1] * 2) + tuple(i[2:]) for i in m]
def m_dim(m):
return [(i[0], max(1, i[1] // 2)) + tuple(i[2:]) for i in m]
def m_inv(m):
# mirror the melody around the tonic (keeps the rhythm)
return [(-i[0], i[1]) for i in m]
def m_shift(m, d):
return [(i[0] + d, i[1]) + tuple(i[2:]) for i in m]
def m_take(m, rows):
"""first `rows` rows of a motif"""
out, r = [], 0
for i in m:
if r >= rows:
break
l = min(i[1], rows - r)
out.append((i[0], l) + tuple(i[2:]))
r += l
return out
def rows_of(m):
return sum(i[1] for i in m)
def chord_tones(deg, mode, root, octave, dominant=False):
"""triad on a scale degree -> list of midi notes"""
base = 12 * octave + root
notes = [base + degree_semis(deg + k, mode) for k in (0, 2, 4)]
if dominant and mode in ("minor", "dorian", "phrygian", "harmonic") and deg % 7 == 4:
notes[1] = base + degree_semis(deg, mode) + 4 # major third on V
return notes
class Gen:
"""Builds a song from a spec dict (see SPECS)."""
def __init__(self, spec):
self.sp = spec
self.rng = random.Random(spec["seed"])
self.root = NOTE_BASE[spec["key"][0]] + (1 if spec["key"].endswith("#") else -1 if spec["key"].endswith("b") and len(spec["key"]) > 1 else 0)
self.mode = spec["mode"]
# ---- accompaniment for a list of chords (degree, rows)
def accompany(self, pat, chords):
sp = self.sp
r = 0
for deg, rows in chords:
notes = chord_tones(deg, self.mode, self.root, 4, dominant=True)
bass = chord_tones(deg, self.mode, self.root, 2, dominant=True)
# chord_tones() counts from C-0 = 0, XM notes from 1
if sp.get("pad"):
pat.set(r, CH_PAD1, notes[1] + 1, sp["pad"], sp.get("pad_vol", 20))
pat.set(r, CH_PAD2, notes[2] + 1, sp["pad"], sp.get("pad_vol", 20))
bp = sp.get("bass_pat", "R.......F.......")
for k in range(rows):
c = bp[(r + k) % len(bp)]
n = {"R": bass[0], "F": bass[2], "O": bass[0] + 12, "T": bass[1]}.get(c)
if n:
pat.set(r + k, CH_BASS, n + 1, sp.get("bass", BASS), sp.get("bass_vol", 44))
elif c == "-":
pat.set(r + k, CH_BASS, KEY_OFF)
if sp.get("arp"):
step = sp.get("arp_step", 2)
order = sp.get("arp_order", (0, 1, 2, 1))
an = chord_tones(deg, self.mode, self.root, sp.get("arp_oct", 5), dominant=True)
for i, rr in enumerate(range(0, rows, step)):
pat.set(r + rr, CH_ARP, an[order[i % len(order)]] + 1, sp["arp"], sp.get("arp_vol", 13))
r += rows
def drums(self, pat, style):
styles = {
"none": {},
"soft": dict(hat="x.......x.......", hv=10),
"calm": dict(kick="x...............", hat="x...x...x...x...", hv=10, kv=44),
"bouncy": dict(kick="x.......x.......", snare="............x...", hat="..x...x...x...x.", hv=13, sv=30),
"rock": dict(kick="x.....x.x.......", snare="....x.......x...", hat="x.x.x.x.x.x.x.x.", hv=15, sv=44),
"driving": dict(kick="x...x...x...x...", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", hv=10, sv=46),
"march": dict(kick="x.......x.......", snare="....x.x.....x.xx", hat="x...x...x...x...", hv=12, sv=38),
"tribal": dict(kick="x..x..x...x..x..", hat="x.x.x.x.x.x.x.x.", hv=11, kv=52),
"boss": dict(kick="x.x.x.x.x.x.x.x.", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", hv=10, sv=50),
}
d = styles[style]
pat.drums(kick=d.get("kick", ""), snare=d.get("snare", ""), hat=d.get("hat", ""), kv=d.get("kv", 60), sv=d.get("sv", 44), hv=d.get("hv", 18))
if style == "tribal":
for bar in range(pat.rows // BAR):
for i in (4, 12, 14):
pat.set(bar * BAR + i, CH_DRUM, note_num("A3" if i == 14 else "D4"), TOM, 46)
if style in ("boss", "driving", "march"):
pat.set(0, CH_HAT, note_num("C4"), CRASH, 34)
# ---- above ground: birds sing now and then, the wind blows softly
def nature(self, pat):
sp = self.sp
if sp.get("wind"):
pat.set(0, CH_WIND, note_num("C4"), WIND, sp.get("wind_vol", 12))
if sp.get("birds"):
calls = ["C4", "D4", "E4", "G4", "A4", "C5"]
for bar in range(pat.rows // BAR):
if self.rng.random() < sp.get("bird_chance", 0.55):
row = bar * BAR + self.rng.choice([2, 5, 7, 10, 13])
pat.set(row, CH_BIRD, note_num(self.rng.choice(calls)), BIRD, 14 + self.rng.randrange(12))
# ---- generated melody over chords (strong beats on chord tones)
def melody(self, chords, inst, vol, octave, style):
rhythms = {
"calm": [[8, 8], [12, 4], [4, 4, 8], [16], [6, 2, 8]],
"flow": [[4, 4, 4, 4], [6, 2, 4, 4], [8, 4, 4], [4, 4, 8]],
"bouncy": [[2, 2, 4, 2, 2, 4], [4, 2, 2, 4, 4], [2, 2, 2, 2, 8], [4, 4, 2, 2, 4]],
"driving": [[2, 2, 2, 2, 2, 2, 4], [4, 2, 2, 4, 4], [2, 2, 4, 2, 2, 4], [2] * 8],
}[style]
scale = MODES[self.mode]
base = 12 * octave + self.root
pool = [base + 12 * o + iv for o in (-1, 0, 1) for iv in scale]
cur = base + scale[0]
out = []
for deg, rows in chords:
ct = [n % 12 for n in chord_tones(deg, self.mode, self.root, 4, dominant=True)]
r = 0
while r < rows:
pat_r = self.rng.choice(rhythms)
for k, l in enumerate(pat_r):
if r >= rows:
break
l = min(l, rows - r)
strong = k == 0 or (r % 8 == 0)
if strong:
cands = [n for n in pool if n % 12 in ct and abs(n - cur) <= 7]
else:
cands = [n for n in pool if 0 < abs(n - cur) <= 3]
if not cands:
cands = [n for n in pool if n % 12 in ct]
# keep the line within about an octave around the centre
cands.sort(key=lambda n: abs(n - cur) + 0.4 * abs(n - (base + 7)) + self.rng.random() * 2)
cur = cands[0]
out.append("%s:%d" % (name_of(cur), l))
r += l
# occasional breath at the end of a phrase
return " ".join(out)
def harm_rows(self, harm, rows_per_bar=BAR):
"""[[0],[3,4],...] -> [(degree, rows)]"""
out = []
for bar in harm:
for d in bar:
out.append((d, rows_per_bar // len(bar)))
return out
def build(self):
sp = self.sp
song = Song(sp["title"], sp["bpm"])
lead, lvol, loct = sp.get("lead", LEAD), sp.get("lead_vol", 40), sp.get("lead_oct", 4)
treat = sp.get("motif", "normal")
prog = sp["prog"] # list of 4 degrees (one bar each) for free sections
# motif treatment
mA, mB = MOTIF_DEG_A, MOTIF_DEG_B
hA, hB = MOTIF_HARM_A, MOTIF_HARM_B
if treat == "inv":
mA, mB = m_inv(mA), m_inv(mB)
elif treat == "shift":
mA, mB = m_shift(mA, 2), m_shift(mB, 2)
sections = [] # (melody string, chords) per 4 bars
if treat in ("normal", "inv", "shift"):
sections.append((realize(mA, self.root, self.mode, loct), self.harm_rows(hA)))
sections.append((realize(mB, self.root, self.mode, loct), self.harm_rows(hB)))
elif treat == "aug":
a = m_aug(m_take(mA, 32))
b = m_aug(m_take(mA[6:], 32)) if False else m_aug(m_take(mA, 64)[len(m_take(mA, 32)):])
sections.append((realize(a, self.root, self.mode, loct), self.harm_rows([[0], [0], [3], [4]])))
sections.append((realize(b, self.root, self.mode, loct), self.harm_rows([[0], [0], [2], [2]])))
elif treat == "dim":
d = m_dim(mA) + m_dim(mB)
sections.append((realize(d, self.root, self.mode, loct + 1), self.harm_rows([[0, 3], [0, 2], [5, 0], [4, 0]])))
elif treat == "frag":
# the motif's first bar as a rising sequence
frag = m_take(mA, 16)
seq = frag + m_shift(frag, 1) + m_shift(frag, 2) + m_take(m_shift(mA, 0)[3:], 16)
sections.append((realize(seq, self.root, self.mode, loct), self.harm_rows([[0], [1], [2], [4]])))
free = self.harm_rows([[d] for d in prog])
mel_style = sp.get("mel_style", "flow")
sections.append((self.melody(free, lead, lvol, loct, mel_style), free))
if sp.get("long"):
free2 = self.harm_rows([[d] for d in sp.get("prog2", prog[::-1])])
sections.append((self.melody(free2, lead, lvol, loct, mel_style), free2))
# intro: accompaniment only
pats = []
intro = Pattern()
self.accompany(intro, free)
self.drums(intro, sp.get("intro_drums", "soft" if sp.get("drums", "none") != "none" else "none"))
self.nature(intro)
pats.append(intro)
for k, (mel, chords) in enumerate(sections):
p = Pattern()
p.line(CH_LEAD, mel, lead, lvol)
if sp.get("echo"):
p.echo(CH_LEAD, CH_COUNTER, sp.get("echo_delay", 3), max(12, lvol // 2), sp.get("echo_inst", SQUARE))
elif sp.get("counter") and k % 2 == 1:
# counter melody: the bass line of the chords an octave up, long notes
cm = " ".join("%s:%d" % (name_of(chord_tones(d, self.mode, self.root, loct - 1, True)[1]), rows) for d, rows in chords)
p.line(CH_COUNTER, cm, sp["counter"], sp.get("counter_vol", 24))
self.accompany(p, chords)
self.drums(p, sp.get("drums", "none"))
self.nature(p)
pats.append(p)
idx = [song.add(x) for x in pats]
song.order = idx + idx[1:]
song.restart = 1
return song
def spec(title, seed, key, mode, bpm, prog, **kw):
d = dict(title=title, seed=seed, key=key, mode=mode, bpm=bpm, prog=prog)
d.update(kw)
return d
# fifteen build tracks in total (three hand-written + twelve generated), calm and varied
BUILD_SPECS = [
spec("Dripping Stone", 4, "A", "minor", 78, [0, 5, 3, 4], motif="aug", lead=HARP, lead_vol=34, pad=CHOIR, bass=SUB, arp=PLUCK, arp_step=4, drums="soft", echo=True, echo_inst=HARP),
spec("Fungus Gardens", 5, "E", "dorian", 96, [0, 3, 0, 6], motif="normal", lead=PLUCK, lead_vol=34, pad=PAD, bass_pat="R.-.R.F.-.R.O.F.", arp=HARP, drums="bouncy", mel_style="bouncy"),
spec("Lantern Path", 6, "G", "minor", 88, [0, 2, 6, 4], motif="frag", lead=SQUARE, lead_vol=32, pad=ORGAN, pad_vol=16, arp=PLUCK, drums="calm", echo=True),
spec("Deep Forge", 7, "C", "phrygian", 92, [0, 1, 0, 6], motif="normal", lead=LEAD, lead_vol=36, pad=PAD, bass=SUB, bass_pat="R.R.....R.R.....", drums="tribal", counter=ORGAN),
spec("Silent Vault", 8, "F", "minor", 72, [0, 5, 2, 4], motif="aug", lead=BELL, lead_vol=40, pad=CHOIR, bass=SUB, bass_pat="R...............", drums="none"),
spec("Crystal Hollow", 9, "B", "minor", 84, [0, 5, 6, 4], motif="inv", lead=BELL, lead_vol=36, pad=PAD, arp=HARP, arp_step=2, arp_order=(0, 1, 2, 3 % 3, 2, 1), drums="soft", echo=True, echo_inst=HARP),
spec("Whispering Halls", 10, "D", "dorian", 80, [0, 3, 6, 0], motif="shift", lead=HARP, lead_vol=34, pad=CHOIR, bass=BASS, drums="calm", long=True, prog2=[3, 0, 4, 0]),
spec("Old Bones", 11, "A", "phrygian", 86, [0, 1, 6, 0], motif="frag", lead=ORGAN, lead_vol=30, pad=PAD, bass=SUB, drums="tribal", arp=PLUCK, arp_step=4),
spec("Ember Chamber", 12, "F#", "minor", 94, [0, 3, 4, 0], motif="normal", lead=LEAD, lead_vol=34, pad=ORGAN, pad_vol=15, arp=PLUCK, drums="calm", counter=SQUARE),
spec("Underground River", 13, "E", "minor", 100, [0, 6, 5, 6], motif="dim", lead=HARP, lead_vol=34, pad=CHOIR, bass_pat="R.R.F.R.R.R.F.O.", arp=PLUCK, arp_step=1, arp_vol=10, drums="soft"),
spec("Moss and Shadow", 14, "G", "dorian", 90, [0, 3, 0, 4], motif="aug", lead=SQUARE, lead_vol=30, pad=PAD, arp=HARP, drums="bouncy", mel_style="bouncy", echo=True),
spec("The Long Dig", 15, "D", "minor", 98, [0, 2, 3, 4], motif="normal", lead=PLUCK, lead_vol=36, pad=PAD, bass_pat="R...R.F.R...F.O.", arp=HARP, drums="bouncy", long=True, prog2=[5, 3, 0, 4]),
]
# seven wave battles (one hand-written + six generated)
BATTLE_SPECS = [
spec("Steel in the Dark", 20, "E", "minor", 144, [0, 5, 6, 4], motif="normal", lead=LEAD, lead_vol=44, pad=PAD, pad_vol=16, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, arp_vol=10, drums="rock", lead_oct=5, mel_style="driving", counter=SQUARE),
spec("Breach!", 21, "C", "harmonic", 152, [0, 5, 3, 4], motif="dim", lead=SQUARE, lead_vol=42, pad=ORGAN, pad_vol=14, bass_pat="RRRRRRRRRRRRRRRR", drums="driving", mel_style="driving"),
spec("Shield Wall", 22, "G", "minor", 138, [0, 3, 6, 4], motif="normal", lead=LEAD, lead_vol=44, pad=CHOIR, bass_pat="R.R.R.F.R.R.F.O.", drums="march", lead_oct=5, mel_style="driving", echo=True),
spec("Torches and Pitchforks", 23, "A", "dorian", 148, [0, 3, 0, 6], motif="frag", lead=SQUARE, lead_vol=40, pad=PAD, pad_vol=15, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, drums="rock", lead_oct=5, mel_style="driving"),
spec("Blood on the Stones", 24, "D", "phrygian", 142, [0, 1, 0, 6], motif="inv", lead=LEAD, lead_vol=44, pad=ORGAN, pad_vol=14, bass_pat="RRRRRRRRRRRRRRRR", drums="tribal", lead_oct=5, mel_style="driving", counter=SQUARE),
spec("No Retreat", 25, "B", "minor", 156, [0, 5, 3, 4], motif="normal", lead=LEAD, lead_vol=44, pad=PAD, pad_vol=15, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, arp_vol=10, drums="driving", lead_oct=5, mel_style="driving", long=True, prog2=[5, 6, 0, 4]),
]
# four gate assaults (one hand-written + three generated)
ASSAULT_SPECS = [
spec("Siege Engine", 30, "D", "harmonic", 160, [0, 5, 1, 4], motif="dim", lead=LEAD, lead_vol=46, pad=ORGAN, pad_vol=15, bass_pat="RRRRRRRRRRRRRRRR", arp=PLUCK, arp_step=1, arp_vol=10, drums="boss", mel_style="driving"),
spec("The Gate Trembles", 31, "F", "minor", 164, [0, 6, 5, 4], motif="frag", lead=SQUARE, lead_vol=44, pad=CHOIR, bass_pat="RRRRRRRRRRRRRRRR", drums="driving", lead_oct=5, mel_style="driving", echo=True, echo_delay=2),
spec("Last Charge", 32, "C", "minor", 168, [0, 3, 4, 0], motif="normal", lead=LEAD, lead_vol=46, pad=PAD, pad_vol=16, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, arp_vol=10, drums="boss", lead_oct=5, mel_style="driving", counter=ORGAN),
]
# six more underground pieces (block 3: wood, walls, drawbridges, siege): four calm, two battles
BUILD_SPECS_2 = [
spec("Root Cellar", 16, "C", "dorian", 86, [0, 3, 4, 0], motif="normal", lead=HARP, lead_vol=34, pad=PAD, arp=PLUCK, arp_step=4, drums="calm", echo=True, echo_inst=HARP),
spec("Glowcap Grove", 17, "E", "minor", 82, [0, 5, 3, 4], motif="aug", lead=BELL, lead_vol=38, pad=CHOIR, bass=SUB, arp=HARP, arp_step=2, drums="soft", echo=True, echo_inst=HARP),
spec("Chains of Iron", 18, "A", "harmonic", 92, [0, 5, 1, 4], motif="frag", lead=ORGAN, lead_vol=30, pad=PAD, bass=SUB, bass_pat="R.R.....R.R.....", drums="tribal", counter=SQUARE),
spec("Mushroom Timber", 19, "G", "minor", 100, [0, 6, 3, 4], motif="normal", lead=PLUCK, lead_vol=36, pad=PAD, bass_pat="R...R.F.R...F.O.", arp=HARP, drums="bouncy", mel_style="bouncy", long=True, prog2=[3, 0, 6, 4]),
]
BATTLE_SPECS_2 = [
spec("Drawbridge Up!", 26, "D", "minor", 150, [0, 6, 3, 4], motif="frag", lead=LEAD, lead_vol=44, pad=PAD, pad_vol=15, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, arp_vol=10, drums="rock", lead_oct=5, mel_style="driving", counter=SQUARE),
spec("Rams at the Door", 27, "F", "phrygian", 146, [0, 1, 6, 0], motif="dim", lead=SQUARE, lead_vol=42, pad=ORGAN, pad_vol=14, bass_pat="RRRRRRRRRRRRRRRR", drums="march", mel_style="driving", echo=True),
]
# above ground: the same leitmotif, brighter (major modes), with flute, harp, birdsong and wind
SURFACE_SPECS = [
spec("First Light", 50, "D", "major", 84, [0, 3, 4, 0], motif="normal", lead=FLUTE, lead_vol=38, pad=CHOIR, pad_vol=14, bass=SUB, arp=HARP, arp_step=2, arp_vol=12, drums="soft", birds=True, wind=True, echo=True, echo_inst=HARP),
spec("Open Sky", 51, "G", "lydian", 90, [0, 1, 4, 0], motif="aug", lead=FLUTE, lead_vol=36, pad=PAD, pad_vol=16, bass=SUB, arp=HARP, arp_step=4, drums="calm", birds=True, bird_chance=0.7),
spec("Forest Path", 52, "A", "mixolydian", 98, [0, 6, 3, 0], motif="frag", lead=FLUTE, lead_vol=38, pad=PAD, pad_vol=14, bass_pat="R...R.F.R...F.O.", arp=PLUCK, arp_step=2, drums="bouncy", mel_style="bouncy", birds=True, wind=True, counter=HARP),
spec("Sunlit Ruins", 53, "F", "major", 78, [0, 5, 3, 4], motif="inv", lead=HARP, lead_vol=36, pad=CHOIR, pad_vol=16, bass=SUB, bass_pat="R.......F.......", arp=FLUTE, arp_step=4, arp_vol=14, drums="soft", birds=True, wind=True, long=True, prog2=[3, 4, 5, 4]),
]
SURFACE_BATTLE_SPECS = [
spec("Banners in the Wind", 54, "D", "mixolydian", 138, [0, 6, 3, 4], motif="normal", lead=FLUTE, lead_vol=44, pad=PAD, pad_vol=15, bass_pat="R.R.R.F.R.R.F.O.", drums="march", lead_oct=5, mel_style="driving", counter=LEAD, wind=True),
spec("Storm the Keep", 55, "E", "dorian", 150, [0, 3, 6, 4], motif="dim", lead=LEAD, lead_vol=44, pad=CHOIR, pad_vol=14, bass_pat="RRRRRRRRRRRRRRRR", arp=HARP, arp_step=1, arp_vol=10, drums="driving", mel_style="driving", echo=True, echo_inst=FLUTE),
]
MOOD_SPECS = {
"torture": spec("The Rack", 40, "C#", "phrygian", 66, [0, 1, 0, 1], motif="aug", lead=BELL, lead_vol=34, pad=CHOIR, pad_vol=16, bass=SUB, bass_pat="R...............", drums="none", intro_drums="none", arp=PLUCK, arp_step=8, arp_order=(0, 1), arp_vol=12),
"temple": spec("Ember Shrine", 41, "D", "lydian", 74, [0, 1, 4, 0], motif="aug", lead=ORGAN, lead_vol=32, pad=CHOIR, pad_vol=20, bass=SUB, bass_pat="R...............", drums="none", intro_drums="none", arp=BELL, arp_step=8, arp_order=(0, 2), arp_vol=18, echo=True, echo_inst=HARP),
}
# ------------------------------------------------------------------ hand-written additions
def song_title2():
"""Throne of Ash: second title theme, the motif slow and majestic on organ and choir."""
s = Song("DungeonCraft - Throne of Ash", 92)
ch_a = bars(("Dm", 1), ("Bb", 1), ("Gm", 1), ("A", 1))
p0 = Pattern()
p0.set(0, CH_DRUM, note_num("C4"), TIMP, 50)
for c, (n, r) in enumerate([("D3", 0), ("D3", 32)]):
p0.set(r, CH_DRUM, note_num("C4"), TIMP, 46)
p0.pads(ch_a, vol=20)
for k in range(4):
p0.set(k * BAR, CH_PAD1, chord_notes(ch_a[k][0], 4)[1], CHOIR, 22)
p0.set(k * BAR, CH_PAD2, chord_notes(ch_a[k][0], 4)[2], CHOIR, 22)
p0.bass(ch_a, "R...............", inst=SUB, vol=46)
p1 = Pattern()
p1.line(CH_LEAD, MOTIF_A, ORGAN, 40)
p1.line(CH_COUNTER, "D3:16 | G2:8 A2:8 | D3:16 | F2:16", BASS, 40)
for k, (c, rr) in enumerate(CHORDS_A):
pass
p1.pads(CHORDS_A, vol=18)
p1.drums(kick="x...............", kv=50)
p1.set(0, CH_HAT, note_num("C4"), CRASH, 30)
p2 = Pattern()
p2.line(CH_LEAD, MOTIF_B, ORGAN, 40)
p2.line(CH_COUNTER, "Bb2:16 | D3:16 | A2:16 | D3:16", BASS, 40)
p2.pads(CHORDS_B, vol=18)
p2.drums(kick="x.......x.......", kv=48)
p3 = Pattern()
p3.line(CH_LEAD, transpose(MOTIF_A, 12), BELL, 40)
p3.echo(CH_LEAD, CH_COUNTER, 4, 20, HARP)
p3.pads(CHORDS_A, vol=20)
p3.bass(CHORDS_A, "R.......F.......", inst=SUB, vol=44)
p3.arp(CHORDS_A, step=2, vol=12, inst=HARP)
p3.drums(kick="x.......x.......", hat="x...x...x...x...", hv=10)
p = [s.add(x) for x in (p0, p1, p2, p3)]
s.order = [p[0], p[1], p[2], p[3], p[2]]
s.restart = 1
return s
def song_boss_paladin():
"""The Paladin of Light: the heroes' fanfare (major) clashes with the leitmotif (minor)."""
s = Song("DungeonCraft - Paladin of Light", 150)
fan_ch = bars(("D", 1), ("G", 1), ("A", 1), ("D", 1))
p0 = Pattern()
p0.line(CH_LEAD, "D5:2 D5:2 D5:2 A5:10 | G5:2 F#5:2 E5:2 D5:10 | E5:4 F#5:4 G5:4 A5:4 | D6:16", SQUARE, 44)
p0.line(CH_COUNTER, "F#4:16 | B4:16 | C#5:16 | F#5:16", SQUARE, 28)
p0.pads(fan_ch, vol=18)
p0.bass(fan_ch, "R...R...R...R...", vol=46)
p0.drums(kick="x.......x.......", snare="....x.xx....xxxx", hat="x.x.x.x.x.x.x.x.", sv=44, hv=12)
p0.set(0, CH_HAT, note_num("C4"), CRASH, 36)
p1 = Pattern()
p1.line(CH_LEAD, transpose(MOTIF_A, 12), LEAD, 46)
p1.line(CH_COUNTER, "D4:2 D4:2 D4:2 A4:10 | G4:8 A4:8 | D4:16 | F4:16", SQUARE, 26)
p1.pads(CHORDS_A, vol=16)
p1.bass(CHORDS_A, "RRRRRRRRRRRRRRRR", vol=44)
p1.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", sv=48, hv=10)
p1.set(0, CH_HAT, note_num("C4"), CRASH, 34)
p2 = Pattern()
p2.line(CH_LEAD, transpose(MOTIF_B, 12), LEAD, 46)
p2.pads(CHORDS_B, vol=16)
p2.bass(CHORDS_B, "RRRRRRRRRRRRRRRR", vol=44)
p2.arp(CHORDS_B, step=1, vol=10)
p2.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x.xx", hat="xxxxxxxxxxxxxxxx", sv=48, hv=10)
p3 = Pattern()
ch3 = bars(("Bb", 1), ("C", 1), ("Dm", 1), ("A", 1))
p3.line(CH_LEAD, "D6:4 C6:4 Bb5:4 A5:4 | G5:4 A5:4 Bb5:4 C6:4 | D6:4 A5:4 F5:4 D5:4 | C#5:8 E5:8", LEAD, 46)
p3.line(CH_COUNTER, "D5:2 D5:2 D5:2 F5:10 | E5:2 E5:2 E5:2 G5:10 | F5:16 | E5:16", SQUARE, 26)
p3.pads(ch3, vol=16)
p3.bass(ch3, "RRRRRRRRRRRRRRRR", vol=44)
p3.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", sv=50, hv=10)
for k in range(4):
p3.set(k * BAR + 14, CH_DRUM, note_num("D4"), TIMP, 50)
p = [s.add(x) for x in (p0, p1, p2, p3)]
s.order = [p[0], p[1], p[2], p[3], p[1], p[2], p[3]]
s.restart = 1
return s
def song_boss_warden():
"""The Gate Warden: heavy, slow-fast, organ and timpani; the motif inverted."""
s = Song("DungeonCraft - Gate Warden", 132)
ch = bars(("Dm", 1), ("Bb", 1), ("Gm", .5), ("A", .5), ("Dm", 1))
inv = realize(m_inv(MOTIF_DEG_A), NOTE_BASE["D"], "harmonic", 5)
p0 = Pattern()
for k in range(4):
p0.set(k * BAR, CH_DRUM, note_num("C4"), TIMP, 56)
p0.set(k * BAR + 8, CH_DRUM, note_num("C4"), TIMP, 46)
p0.line(CH_PAD1, "D4:32 | C#4:32", ORGAN, 24)
p0.line(CH_PAD2, "A4:32 | E4:32", ORGAN, 24)
p0.bass(ch, "R...............", inst=SUB, vol=50)
p1 = Pattern()
p1.line(CH_LEAD, inv, LEAD, 46)
p1.pads(ch, vol=18)
p1.bass(ch, "R.R.R.R.R.R.R.R.", vol=46)
p1.drums(kick="x..x..x.x..x..x.", snare="....x.......x...", hat="x.x.x.x.x.x.x.x.", sv=48, hv=12)
p1.set(0, CH_HAT, note_num("C4"), CRASH, 34)
p2 = Pattern()
p2.line(CH_LEAD, transpose(MOTIF_A, 12), ORGAN, 42)
p2.echo(CH_LEAD, CH_COUNTER, 2, 22, SQUARE)
p2.pads(CHORDS_A, vol=18)
p2.bass(CHORDS_A, "RRRRRRRRRRRRRRRR", vol=46)
p2.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x.xx", hat="xxxxxxxxxxxxxxxx", sv=50, hv=10)
p = [s.add(x) for x in (p0, p1, p2)]
s.order = [p[0], p[1], p[2], p[1], p[2]]
s.restart = 1
return s
def song_victory2():
"""Crown of the Deep: a slower, triumphant victory jingle with bells."""
s = Song("DungeonCraft - Crown of the Deep", 100)
p0 = Pattern()
ch = bars(("D", 1), ("Bb", .5), ("C", .5), ("D", 2))
p0.line(CH_LEAD, "A4:4 D5:4 F#5:4 A5:4 | Bb5:6 A5:2 G5:4 E5:4 | D5:32", BELL, 48)
p0.line(CH_COUNTER, "D4:16 | D4:8 E4:8 | F#4:32", ORGAN, 30)
p0.pads(ch, vol=22)
p0.bass(ch, "R.......R.......", inst=SUB, vol=46)
for k in (0, 32, 40, 44):
p0.set(k, CH_DRUM, note_num("C4"), TIMP, 52)
p0.set(32, CH_HAT, note_num("C4"), CRASH, 36)
for c in range(BASE_CHANNELS):
p0.set(63, c, KEY_OFF)
silence = Pattern(1)
p = [s.add(x) for x in (p0, silence)]
s.order = [p[0], p[1]]
s.restart = 1
return s
# ------------------------------------------------------------------ holiday pieces
# During a holiday the title music (and now and then the build music) plays the holiday's piece
# (see audio::holiday_category). Each one quotes the leitmotif in its own colours.
def sleigh(pat, vol=8):
"""Sleigh bells: the hihat pitched up, every eighth, louder on the beats."""
for r in range(0, pat.rows, 2):
pat.set(r, CH_HAT, note_num("C6"), HAT, vol + (6 if r % 4 == 0 else 0))
def organ_pads(pat, chords, vol=18, octave=4):
r = 0
for name, rows in chords:
notes = chord_notes(name, octave)
pat.set(r, CH_PAD1, notes[1], ORGAN, vol)
pat.set(r, CH_PAD2, notes[2], ORGAN, vol)
r += rows
def choir_pads(pat, chords, vol=18, octave=4):
r = 0
for name, rows in chords:
notes = chord_notes(name, octave)
pat.set(r, CH_PAD1, notes[1], CHOIR, vol)
pat.set(r, CH_PAD2, notes[2], CHOIR, vol)
r += rows
def song_christmas():
"""Frostbound Carol: a dark carol in D minor on bells and choir, sleigh bells, a turn to
D major in the middle that falls back into the minor; the leitmotif rung on the bells."""
s = Song("Frostbound Carol", 100)
mel_a = "A4:4 A4:4 Bb4:4 A4:4 | G4:4 F4:4 E4:8 | F4:4 G4:4 A4:4 D5:4 | C#5:12 -:4"
mel_b = "D5:4 C5:4 Bb4:4 A4:4 | Bb4:4 A4:4 G4:8 | F4:4 E4:4 D4:4 F4:4 | E4:6 C#4:2 D4:8"
mel_c = "F#4:4 A4:4 D5:8 | B4:4 A4:4 G4:8 | F#4:4 G4:4 A4:4 B4:4 | A4:12 -:4"
ch_a = bars(("Dm", 1), ("Gm", .5), ("A", .5), ("Dm", .5), ("F", .5), ("A", 1))
ch_b = bars(("Bb", 1), ("Gm", 1), ("Dm", 1), ("A", .5), ("Dm", .5))
ch_c = bars(("D", 1), ("G", 1), ("D", .5), ("G", .5), ("A", 1))
ch_i = bars(("Dm", 1), ("Bb", 1), ("Gm", 1), ("A", 1))
# intro: the bells toll over the choir, timpani like distant steps in the snow
p0 = Pattern()
p0.line(CH_LEAD, "D5:16 | D5:16 | D5:16 | C#5:16", BELL, 36)
choir_pads(p0, ch_i, vol=20)
p0.bass(ch_i, "R...............", inst=SUB, vol=42)
for k in (0, 32):
p0.set(k, CH_DRUM, note_num("C4"), TIMP, 44)
p1 = Pattern()
p1.line(CH_LEAD, transpose(mel_a, 12), BELL, 44)
choir_pads(p1, ch_a, vol=18)
p1.bass(ch_a, "R.......F.......", vol=40)
sleigh(p1, 6)
p2 = Pattern()
p2.line(CH_LEAD, transpose(mel_b, 12), BELL, 44)
choir_pads(p2, ch_b, vol=18)
p2.bass(ch_b, "R.......F.......", vol=40)
sleigh(p2, 6)
# second verse: the organ sings, the harp ripples, a soft kick
p3 = Pattern()
p3.line(CH_LEAD, mel_a, ORGAN, 38)
p3.line(CH_COUNTER, "D4:16 | Bb3:8 C#4:8 | D4:8 C4:8 | C#4:16", CHOIR, 22)
p3.pads(ch_a, vol=14)
p3.bass(ch_a, "R...R.F.R...F.O.", vol=40)
p3.arp(ch_a, step=2, vol=11, inst=HARP)
p3.drums(kick="x.......x.......", kv=44)
sleigh(p3, 8)
p4 = Pattern()
p4.line(CH_LEAD, mel_b, ORGAN, 38)
p4.line(CH_COUNTER, "F4:16 | D4:16 | A3:16 | G3:8 F3:8", CHOIR, 22)
p4.pads(ch_b, vol=14)
p4.bass(ch_b, "R...R.F.R...F.O.", vol=40)
p4.arp(ch_b, step=2, vol=11, inst=HARP)
p4.drums(kick="x.......x.......", kv=44)
sleigh(p4, 8)
# the turn to major: candle light for a moment
p5 = Pattern()
p5.line(CH_LEAD, transpose(mel_c, 12), BELL, 46)
p5.line(CH_COUNTER, "D4:16 | D4:16 | D4:8 B3:8 | C#4:16", ORGAN, 26)
choir_pads(p5, ch_c, vol=18)
p5.bass(ch_c, "R.......F.......", vol=42)
p5.arp(ch_c, step=4, vol=10, inst=HARP)
sleigh(p5, 8)
p5.set(48, CH_DRUM, note_num("C4"), TIMP, 40)
# the leitmotif on the bells, echoed by the harp
p6 = Pattern()
p6.line(CH_LEAD, transpose(MOTIF_A, 12), BELL, 42)
p6.echo(CH_LEAD, CH_COUNTER, 4, 18, HARP)
choir_pads(p6, CHORDS_A, vol=18)
p6.bass(CHORDS_A, "R.......F.......", inst=SUB, vol=42)
sleigh(p6, 6)
p7 = Pattern()
p7.line(CH_LEAD, transpose(MOTIF_B, 12), BELL, 42)
p7.echo(CH_LEAD, CH_COUNTER, 4, 18, HARP)
choir_pads(p7, CHORDS_B, vol=18)
p7.bass(CHORDS_B, "R.......F.......", inst=SUB, vol=42)
sleigh(p7, 6)
p7.set(48, CH_DRUM, note_num("C4"), TIMP, 42)
p = [s.add(x) for x in (p0, p1, p2, p3, p4, p5, p6, p7)]
s.order = [p[0], p[1], p[2], p[3], p[4], p[5], p[2], p[6], p[7], p[1], p[2]]
s.restart = 1
return s
def song_easter():
"""Eggs in the Moss: light and playful in G major, flute and harp, birdsong, a bouncing
beat; the leitmotif turned into a spring tune."""
s = Song("Eggs in the Moss", 120)
rng = random.Random(61)
mel_a = "G4:2 B4:2 D5:4 B4:2 D5:2 G5:4 | E5:2 D5:2 C5:2 B4:2 A4:8 | F#4:2 A4:2 D5:4 C5:2 B4:2 A4:4 | B4:4 G4:4 G4:8"
mel_b = "E5:4 D5:2 C5:2 B4:4 A4:4 | D5:4 C5:2 B4:2 A4:8 | G4:2 A4:2 B4:2 C5:2 D5:4 E5:4 | D5:8 -:8"
ch_a = bars(("G", 1), ("C", .5), ("D", .5), ("D", 1), ("G", 1))
ch_b = bars(("Em", 1), ("D", 1), ("C", .5), ("G", .5), ("D", 1))
mot_a = realize(MOTIF_DEG_A, NOTE_BASE["G"], "major", 4)
mot_b = realize(MOTIF_DEG_B, NOTE_BASE["G"], "major", 4)
ch_ma = bars(("G", 1), ("C", .5), ("D", .5), ("G", 1), ("Bm", 1))
ch_mb = bars(("Em", 1), ("G", 1), ("D", 1), ("G", 1))
bouncy = dict(kick="x.......x.......", snare="............x...", hat="..x...x...x...x.", hv=12, sv=28)
def birds(pat):
for bar in range(pat.rows // BAR):
if rng.random() < 0.6:
row = bar * BAR + rng.choice([2, 5, 7, 10, 13])
pat.set(row, CH_BIRD, note_num(rng.choice(["C4", "D4", "E4", "G4", "A4", "C5"])), BIRD, 12 + rng.randrange(10))
def verse(mel, ch, lead, lvol, echo=None):
p = Pattern()
p.line(CH_LEAD, mel, lead, lvol)
if echo:
p.echo(CH_LEAD, CH_COUNTER, 2, 16, echo)
p.pads(ch, vol=13)
p.bass(ch, "R.-.R.F.-.R.O.F.", vol=40)
p.arp(ch, step=2, vol=11, inst=HARP, order=(0, 1, 2, 1))
p.drums(**bouncy)
birds(p)
return p
p0 = Pattern()
choir_pads(p0, ch_a, vol=12)
p0.bass(ch_a, "R.......F.......", inst=SUB, vol=38)
p0.arp(ch_a, step=2, vol=11, inst=HARP, order=(0, 1, 2, 1))
p0.drums(hat="..x...x...x...x.", hv=10)
birds(p0)
p0.set(0, CH_WIND, note_num("C4"), WIND, 10)
p1 = verse(mel_a, ch_a, FLUTE, 40)
p2 = verse(mel_b, ch_b, FLUTE, 40)
p3 = verse(transpose(mel_a, 12), ch_a, PLUCK, 34, HARP)
p4 = verse(transpose(mel_b, 12), ch_b, PLUCK, 34, HARP)
p5 = verse(mot_a, ch_ma, FLUTE, 40, HARP)
p6 = verse(mot_b, ch_mb, FLUTE, 40, HARP)
p = [s.add(x) for x in (p0, p1, p2, p3, p4, p5, p6)]
s.order = [p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[3], p[4], p[1], p[2]]
s.restart = 1
return s
def song_halloween():
"""Crypt of Lanterns: a spooky dance in D harmonic minor, harpsichord and organ, midnight
bells, toms like knocking on coffin lids; the leitmotif on the harpsichord."""
s = Song("Crypt of Lanterns", 120)
mel_a = "D4:4 F4:4 G#4:4 A4:4 | Bb4:6 A4:2 G#4:8 | A4:4 C5:4 Bb4:4 G4:4 | A4:16"
mel_b = "D5:2 Eb5:2 D5:4 A4:4 F4:4 | G4:2 F4:2 E4:4 C#4:8 | D4:2 E4:2 F4:2 G4:2 A4:2 Bb4:2 C#5:4 | D5:16"
ch_a = bars(("Dm", 1), ("E", 1), ("Gm", 1), ("A", 1))
ch_b = bars(("Dm", 1), ("A", 1), ("Gm", .5), ("A", .5), ("Dm", 1))
def knock(pat):
for bar in range(pat.rows // BAR):
pat.set(bar * BAR + 14, CH_DRUM, note_num("A3"), TOM, 36)
# midnight: twelve... well, four bells over an organ drone
p0 = Pattern()
p0.line(CH_LEAD, "D4:16 | D4:16 | D4:16 | C#4:16", BELL, 36)
p0.line(CH_PAD1, "D3:64", ORGAN, 20)
p0.line(CH_PAD2, "A3:32 | G#3:32", ORGAN, 18)
p0.bass(ch_a, "R...............", inst=SUB, vol=44)
p0.arp(ch_a, step=4, vol=9, octave=4, inst=HARPSI)
def verse(mel, ch, lead, lvol, heavy=False):
p = Pattern()
p.line(CH_LEAD, mel, lead, lvol)
organ_pads(p, ch, vol=16)
p.bass(ch, "R.......R...F..." if not heavy else "R.R.....R.R.F...", inst=SUB, vol=44)
p.arp(ch, step=1, vol=10, octave=4, order=(0, 1, 2, 1), inst=HARPSI)
if heavy:
p.drums(kick="x.....x.x.......", snare="....x.......x...", hat="..x...x...x...x.", kv=52, sv=34, hv=10)
else:
p.drums(kick="x.......x.......", hat="..x...x...x...x.", kv=48, hv=9)
knock(p)
return p
p1 = verse(transpose(mel_a, 12), ch_a, HARPSI, 40)
p2 = verse(transpose(mel_b, 12), ch_b, HARPSI, 40)
p3 = verse(mel_a, ch_a, ORGAN, 36, heavy=True)
p3.line(CH_COUNTER, "A3:16 | G#3:16 | Bb3:16 | C#4:16", HARPSI, 24)
p4 = verse(mel_b, ch_b, ORGAN, 36, heavy=True)
p4.line(CH_COUNTER, "F3:16 | E3:16 | D3:8 E3:8 | F3:16", HARPSI, 24)
# the leitmotif on the harpsichord with its own ghostly echo
p5 = verse(transpose(MOTIF_A, 12), CHORDS_A, HARPSI, 40)
p5.echo(CH_LEAD, CH_COUNTER, 3, 16, HARPSI)
p6 = verse(transpose(MOTIF_B, 12), CHORDS_B, HARPSI, 40)
p6.echo(CH_LEAD, CH_COUNTER, 3, 16, HARPSI)
# creeping down: a chromatic organ line over a pedal, the bells again
p7 = Pattern()
p7.line(CH_LEAD, "A4:8 G#4:8 | G4:8 F#4:8 | F4:8 E4:8 | Eb4:8 D4:8", ORGAN, 34)
p7.line(CH_PAD1, "D3:64", ORGAN, 16)
p7.line(CH_PAD2, "A3:64", ORGAN, 14)
p7.bass(ch_a, "R...............", inst=SUB, vol=44)
for k in (0, 32):
p7.set(k, CH_COUNTER, note_num("D5"), BELL, 30)
p7.set(k, CH_DRUM, note_num("C4"), TIMP, 46)
p7.arp(ch_a, step=2, vol=9, octave=4, inst=HARPSI, order=(0, 2))
p = [s.add(x) for x in (p0, p1, p2, p3, p4, p5, p6, p7)]
s.order = [p[0], p[1], p[2], p[3], p[4], p[7], p[5], p[6], p[1], p[2], p[3], p[4]]
s.restart = 1
return s
def generated(spec_):
return lambda: Gen(spec_).build()
# name -> (category, song function); the order within a category is the play order
SONGS = {}
SONGS["title"] = ("title", song_title)
SONGS["title2"] = ("title", song_title2)
SONGS["build1"] = ("build", song_build1)
SONGS["build2"] = ("build", song_build2)
SONGS["build3"] = ("build", song_build3)
for k, sp_ in enumerate(BUILD_SPECS):
SONGS["build%d" % (k + 4)] = ("build", generated(sp_))
SONGS["battle1"] = ("battle", song_battle1)
for k, sp_ in enumerate(BATTLE_SPECS):
SONGS["battle%d" % (k + 3)] = ("battle", generated(sp_))
for k, sp_ in enumerate(BUILD_SPECS_2):
SONGS["build%d" % (k + 16)] = ("build", generated(sp_))
for k, sp_ in enumerate(BATTLE_SPECS_2):
SONGS["battle%d" % (k + 9)] = ("battle", generated(sp_))
for k, sp_ in enumerate(SURFACE_SPECS):
SONGS["surface%d" % (k + 1)] = ("surface", generated(sp_))
for k, sp_ in enumerate(SURFACE_BATTLE_SPECS):
SONGS["surface_battle%d" % (k + 1)] = ("surface_battle", generated(sp_))
SONGS["battle2"] = ("assault", song_battle2)
for k, sp_ in enumerate(ASSAULT_SPECS):
SONGS["assault%d" % (k + 2)] = ("assault", generated(sp_))
SONGS["boss_paladin"] = ("boss_paladin", song_boss_paladin)
SONGS["boss_warden"] = ("boss_warden", song_boss_warden)
SONGS["mood_torture"] = ("torture", generated(MOOD_SPECS["torture"]))
SONGS["mood_temple"] = ("temple", generated(MOOD_SPECS["temple"]))
SONGS["victory"] = ("victory", song_victory)
SONGS["victory2"] = ("victory", song_victory2)
SONGS["defeat"] = ("defeat", song_defeat)
SONGS["holiday_christmas"] = ("christmas", song_christmas)
SONGS["holiday_easter"] = ("easter", song_easter)
SONGS["holiday_halloween"] = ("halloween", song_halloween)
def write_rust_table(names):
"""src/tracks.rs: embedded music with categories (generated, do not edit)"""
lines = [
"// Generated by tools/compose_music.py - do not edit.",
"// All music modules with their category (see audio::Category).",
"",
"use crate::audio::Category;",
"",
"pub const TRACKS: [(&str, Category, &[u8]); %d] = [" % len(names),
]
cat = {"title": "Title", "build": "Build", "battle": "Battle", "assault": "Assault", "boss_paladin": "BossPaladin",
"boss_warden": "BossWarden", "torture": "Torture", "temple": "Temple", "victory": "Victory", "defeat": "Defeat",
"surface": "Surface", "surface_battle": "SurfaceBattle", "christmas": "Christmas", "easter": "Easter",
"halloween": "Halloween"}
for n in names:
lines.append(' ("%s", Category::%s, include_bytes!("../assets/music/%s.xm")),' % (n, cat[SONGS[n][0]], n))
lines.append("];")
path = os.path.join(os.path.dirname(__file__), "..", "src", "tracks.rs")
with open(path, "w") as f:
f.write("\n".join(lines) + "\n")
def main():
os.makedirs(OUT_DIR, exist_ok=True)
instruments = build_instruments()
total = 0
for name, (cat, fn) in SONGS.items():
song = fn()
size = write_xm(os.path.join(OUT_DIR, name + ".xm"), song, instruments)
total += size
print("%-13s %-12s %3d BPM %2d patterns %6d bytes %s" % (name, cat, song.bpm, len(song.patterns), size, song.name))
write_rust_table(list(SONGS.keys()))
print("%d songs, %d KB" % (len(SONGS), total // 1024))
if __name__ == "__main__":
main()