Soundtrack expanded to 35 pieces with a leitmotif variant generator and 8 new instruments
- generator: leitmotif as scale degrees, keys/modes, augmentation, diminution, inversion, fragment sequences, accompaniment and drum styles; hand-written title 2, boss themes, victory 2 - organ, bell, choir, sub bass, tom, crash, harp, timpani; 8-bit samples, unused instruments stripped - src/tracks.rs generated with categories; music director with boss themes and room moods - all pieces validated with libopenmpt; notes checked to stay in key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e92e19089
commit
5158075d7b
42 files changed
+721
-98
No files matched your search
Binary file not shown.
+583
-24
@@ -180,6 +180,84 @@ def hihat():
|
||||
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 build_instruments():
|
||||
return [
|
||||
Instrument("Lead pulse 25%", Sample("pulse25", pulse(0.25), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
||||
@@ -195,10 +273,23 @@ def build_instruments():
|
||||
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)),
|
||||
]
|
||||
|
||||
|
||||
LEAD, SQUARE, PAD, BASS, PLUCK, KICK, SNARE, HAT = range(1, 9)
|
||||
ORGAN, BELL, CHOIR, SUB, TOM, CRASH, HARP, TIMP = range(9, 17)
|
||||
# channel layout
|
||||
CH_LEAD, CH_COUNTER, CH_PAD1, CH_PAD2, CH_BASS, CH_ARP, CH_DRUM, CH_HAT = range(8)
|
||||
CHANNELS = 8
|
||||
@@ -601,6 +692,10 @@ def song_defeat():
|
||||
|
||||
|
||||
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" ")
|
||||
@@ -609,7 +704,7 @@ def write_xm(path, song, instruments):
|
||||
out += struct.pack("<H", 0x0104)
|
||||
order = song.order + [0] * (256 - len(song.order))
|
||||
out += struct.pack("<IHHHHHHHH", 276, len(song.order), song.restart, CHANNELS, len(song.patterns),
|
||||
len(instruments), 1, song.speed, song.bpm)
|
||||
len(used), 1, song.speed, song.bpm)
|
||||
out += bytes(order)
|
||||
for pat in song.patterns:
|
||||
data = bytearray()
|
||||
@@ -619,10 +714,11 @@ def write_xm(path, song, instruments):
|
||||
data += b"\x80"
|
||||
else:
|
||||
note, inst, vol, fx, param = c
|
||||
data += bytes([note, inst, vol, fx, param])
|
||||
data += bytes([note, remap.get(inst, 0), vol, fx, param])
|
||||
out += struct.pack("<IBHH", 9, 0, pat.rows, len(data))
|
||||
out += data
|
||||
for ins in instruments:
|
||||
for old in used:
|
||||
ins = instruments[old - 1]
|
||||
smp = ins.sample
|
||||
hdr = bytearray()
|
||||
hdr += ins.name.encode("ascii")[:22].ljust(22, b"\0")
|
||||
@@ -634,8 +730,7 @@ def write_xm(path, song, instruments):
|
||||
pts = bytearray()
|
||||
for (x, y) in env[:12]:
|
||||
pts += struct.pack("<HH", x, y)
|
||||
pts = pts.ljust(48, b"\0")
|
||||
hdr += pts
|
||||
hdr += pts.ljust(48, b"\0")
|
||||
hdr += bytes(48) # panning envelope
|
||||
vol_type = 0
|
||||
if env:
|
||||
@@ -650,47 +745,511 @@ def write_xm(path, song, instruments):
|
||||
hdr += bytes(22) # reserved
|
||||
out += struct.pack("<I", 4 + len(hdr))
|
||||
out += hdr
|
||||
# sample header (16-bit)
|
||||
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 * 2, ls * 2, ll * 2, smp.volume, smp.finetune, loop_type | 0x10, smp.pan,
|
||||
smp.relnote, 0)
|
||||
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")
|
||||
# delta encoded 16-bit sample data
|
||||
prev = 0
|
||||
for v in smp.data:
|
||||
s16 = int(round(max(-1.0, min(1.0, v)) * 32767))
|
||||
d = (s16 - prev) & 0xFFFF
|
||||
out += struct.pack("<H", d)
|
||||
prev = s16
|
||||
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)
|
||||
|
||||
|
||||
SONGS = {
|
||||
"title": song_title,
|
||||
"build1": song_build1,
|
||||
"build2": song_build2,
|
||||
"build3": song_build3,
|
||||
"battle1": song_battle1,
|
||||
"battle2": song_battle2,
|
||||
"victory": song_victory,
|
||||
"defeat": song_defeat,
|
||||
# ------------------------------------------------------------------ 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)
|
||||
|
||||
# ---- 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"))
|
||||
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"))
|
||||
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),
|
||||
]
|
||||
|
||||
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(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 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_))
|
||||
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)
|
||||
|
||||
|
||||
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"}
|
||||
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()
|
||||
for name, fn in SONGS.items():
|
||||
total = 0
|
||||
for name, (cat, fn) in SONGS.items():
|
||||
song = fn()
|
||||
size = write_xm(os.path.join(OUT_DIR, name + ".xm"), song, instruments)
|
||||
print("%-8s %3d BPM %2d patterns %6d bytes" % (name, song.bpm, len(song.patterns), size))
|
||||
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__":
|
||||
|
||||
Reference in new issue
Block a user