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:
1 parent
86c707e151
commit
d54de78f37
19 files changed
+166
-16
No files matched your search
+110
-9
@@ -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("];")
|
||||
|
||||
Reference in new issue
Block a user