Soundtrack: XM composer script and 8 generated chiptune modules
tools/compose_music.py synthesises chip instruments and writes FastTracker 2 XM files (editable in MilkyTracker). The title leitmotif runs through all pieces: title, three build tracks, two battle tracks, victory and defeat jingles. Validated with libopenmpt; instrument tuning checked (A4 = 440 Hz). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fb7dab3683
commit
2dba65be8b
9 files changed
+697
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+697
@@ -0,0 +1,697 @@
|
||||
#!/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"],
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)),
|
||||
]
|
||||
|
||||
|
||||
LEAD, SQUARE, PAD, BASS, PLUCK, KICK, SNARE, HAT = range(1, 9)
|
||||
# channel layout
|
||||
CH_LEAD, CH_COUNTER, CH_PAD1, CH_PAD2, CH_BASS, CH_ARP, CH_DRUM, CH_HAT = range(8)
|
||||
CHANNELS = 8
|
||||
|
||||
# ------------------------------------------------------------------ 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(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(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):
|
||||
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))
|
||||
out += struct.pack("<IHHHHHHHH", 276, len(song.order), song.restart, CHANNELS, len(song.patterns),
|
||||
len(instruments), 1, song.speed, song.bpm)
|
||||
out += bytes(order)
|
||||
for pat in song.patterns:
|
||||
data = bytearray()
|
||||
for row in pat.cells:
|
||||
for c in row:
|
||||
if c is None:
|
||||
data += b"\x80"
|
||||
else:
|
||||
note, inst, vol, fx, param = c
|
||||
data += bytes([note, inst, vol, fx, param])
|
||||
out += struct.pack("<IBHH", 9, 0, pat.rows, len(data))
|
||||
out += data
|
||||
for ins in instruments:
|
||||
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)
|
||||
pts = pts.ljust(48, b"\0")
|
||||
hdr += pts
|
||||
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
|
||||
# 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 += 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
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
instruments = build_instruments()
|
||||
for name, 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))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user