Music: six more dungeon pieces and six surface pieces with flute, harp, birdsong and wind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 22:58:41 +01:00
1 parent 86c707e151
commit d54de78f37
19 files changed
+166 -16

No files matched your search

+110 -9
View File
@@ -258,6 +258,52 @@ def timpani():
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 build_instruments():
return [
Instrument("Lead pulse 25%", Sample("pulse25", pulse(0.25), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
@@ -285,14 +331,21 @@ def build_instruments():
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),
]
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
FLUTE, BIRD, WIND = range(17, 20)
# 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
@@ -661,7 +714,7 @@ def song_victory():
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):
for c in range(BASE_CHANNELS):
p0.set(63, c, KEY_OFF)
silence = Pattern(1)
p = [s.add(x) for x in (p0, silence)]
@@ -679,7 +732,7 @@ def song_defeat():
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):
for c in range(BASE_CHANNELS):
p0.set(63, c, KEY_OFF)
silence = Pattern(1)
p = [s.add(x) for x in (p0, silence)]
@@ -703,13 +756,14 @@ def write_xm(path, song, instruments):
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),
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:
for c in row[:nch]:
if c is None:
data += b"\x80"
else:
@@ -910,6 +964,18 @@ class Gen:
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 = {
@@ -996,6 +1062,7 @@ class Gen:
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()
@@ -1008,6 +1075,7 @@ class Gen:
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:]
@@ -1054,6 +1122,30 @@ ASSAULT_SPECS = [
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),
@@ -1182,7 +1274,7 @@ def song_victory2():
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):
for c in range(BASE_CHANNELS):
p0.set(63, c, KEY_OFF)
silence = Pattern(1)
p = [s.add(x) for x in (p0, silence)]
@@ -1207,6 +1299,14 @@ for k, sp_ in enumerate(BUILD_SPECS):
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_))
@@ -1230,7 +1330,8 @@ def write_rust_table(names):
"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"}
"boss_warden": "BossWarden", "torture": "Torture", "temple": "Temple", "victory": "Victory", "defeat": "Defeat",
"surface": "Surface", "surface_battle": "SurfaceBattle"}
for n in names:
lines.append(' ("%s", Category::%s, include_bytes!("../assets/music/%s.xm")),' % (n, cat[SONGS[n][0]], n))
lines.append("];")