diff --git a/assets/music/battle1.xm b/assets/music/battle1.xm new file mode 100644 index 0000000..439f6a3 Binary files /dev/null and b/assets/music/battle1.xm differ diff --git a/assets/music/battle2.xm b/assets/music/battle2.xm new file mode 100644 index 0000000..9854e2e Binary files /dev/null and b/assets/music/battle2.xm differ diff --git a/assets/music/build1.xm b/assets/music/build1.xm new file mode 100644 index 0000000..2538dcf Binary files /dev/null and b/assets/music/build1.xm differ diff --git a/assets/music/build2.xm b/assets/music/build2.xm new file mode 100644 index 0000000..9894c02 Binary files /dev/null and b/assets/music/build2.xm differ diff --git a/assets/music/build3.xm b/assets/music/build3.xm new file mode 100644 index 0000000..13f81a7 Binary files /dev/null and b/assets/music/build3.xm differ diff --git a/assets/music/defeat.xm b/assets/music/defeat.xm new file mode 100644 index 0000000..36ef422 Binary files /dev/null and b/assets/music/defeat.xm differ diff --git a/assets/music/title.xm b/assets/music/title.xm new file mode 100644 index 0000000..adb092a Binary files /dev/null and b/assets/music/title.xm differ diff --git a/assets/music/victory.xm b/assets/music/victory.xm new file mode 100644 index 0000000..78f261f Binary files /dev/null and b/assets/music/victory.xm differ diff --git a/tools/compose_music.py b/tools/compose_music.py new file mode 100755 index 0000000..6df481b --- /dev/null +++ b/tools/compose_music.py @@ -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(" sample 0 + env = ins.env or [] + pts = bytearray() + for (x, y) in env[:12]: + pts += struct.pack("