Audio: mood music, ambience, sound effects and creature voices (Kenney CC0 + synthesised); protocol 13
This commit is contained in:
BIN
tools/audio/__pycache__/make_audio.cpython-314.pyc
Normal file
BIN
tools/audio/__pycache__/make_audio.cpython-314.pyc
Normal file
Binary file not shown.
783
tools/audio/make_audio.py
Normal file
783
tools/audio/make_audio.py
Normal file
@@ -0,0 +1,783 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Emberwild's own sounds: the music, the ambience beds and the sound effects
|
||||
that the Kenney CC0 packs don't cover, all synthesised here (no samples, no
|
||||
third-party audio), so they're ours to release under CC0 too.
|
||||
|
||||
python3 tools/audio/make_audio.py # everything
|
||||
python3 tools/audio/make_audio.py music # or: music | ambience | sfx | voices
|
||||
python3 tools/audio/make_audio.py --kenney ~/Downloads/kenney # also re-copy the Kenney files
|
||||
|
||||
Writes .ogg files under assets/audio/ (via ffmpeg). Deterministic: the same
|
||||
seeds give the same files. Pure Python (no numpy), so it takes a minute or two.
|
||||
|
||||
Music (assets/audio/music/): calm (day), night, storm, danger, boss. Each is
|
||||
a seamless loop: the tail of the last bar is folded back onto the start.
|
||||
"""
|
||||
import math, os, random, shutil, struct, subprocess, sys, tempfile, wave
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
OUT = os.path.join(ROOT, "assets", "audio")
|
||||
TAU = 2.0 * math.pi
|
||||
sin, exp = math.sin, math.exp
|
||||
|
||||
|
||||
def midi(m):
|
||||
return 440.0 * 2.0 ** ((m - 69) / 12.0)
|
||||
|
||||
|
||||
# --- writing -----------------------------------------------------------------------
|
||||
|
||||
def _encode(path, chans, sr, q=4):
|
||||
"""chans: [left, right] or [mono] float lists. Peak-normalised, soft-clipped."""
|
||||
peak = max(max(abs(x) for x in c) for c in chans) or 1.0
|
||||
g = 0.89 / peak
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
||||
with wave.open(tmp, "wb") as w:
|
||||
w.setnchannels(len(chans))
|
||||
w.setsampwidth(2)
|
||||
w.setframerate(sr)
|
||||
frames = bytearray()
|
||||
n = len(chans[0])
|
||||
for i in range(n):
|
||||
for c in chans:
|
||||
v = math.tanh(c[i] * g * 1.05) / math.tanh(1.05)
|
||||
frames += struct.pack("<h", int(max(-1.0, min(1.0, v)) * 32000))
|
||||
w.writeframes(bytes(frames))
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
# (ffmpeg's own Vorbis encoder only writes stereo: a mono sound gets two
|
||||
# identical channels, which Godot plays the same.)
|
||||
subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", tmp, "-ac", "2", "-c:a", "vorbis", "-strict", "-2",
|
||||
"-q:a", str(q), path], check=True)
|
||||
os.remove(tmp)
|
||||
print(" %s (%.1f s, %d KB)" % (os.path.relpath(path, ROOT), n / sr, os.path.getsize(path) // 1024))
|
||||
|
||||
|
||||
# --- voices (each adds into a buffer at a start time) ---------------------------------
|
||||
|
||||
def add_pluck(buf, sr, t0, freq, amp, decay=2.2, harm=(1.0, 0.45, 0.22, 0.1), dur=None):
|
||||
"""A soft plucked string / harp-ish note."""
|
||||
i0 = int(t0 * sr)
|
||||
dur = dur or min(4.5, 7.0 / decay)
|
||||
n = int(dur * sr)
|
||||
ph = [0.0] * len(harm)
|
||||
inc = [TAU * freq * (k + 1) / sr for k in range(len(harm))]
|
||||
dec = [exp(-decay * (1.0 + 0.6 * k) / sr) for k in range(len(harm))]
|
||||
env = [h * amp for h in harm]
|
||||
L = len(buf)
|
||||
att = int(0.004 * sr)
|
||||
for j in range(n):
|
||||
s = 0.0
|
||||
for k in range(len(harm)):
|
||||
s += env[k] * sin(ph[k])
|
||||
ph[k] += inc[k]
|
||||
env[k] *= dec[k]
|
||||
if j < att:
|
||||
s *= j / att
|
||||
buf[(i0 + j) % L] += s
|
||||
|
||||
|
||||
def add_pad(buf, sr, t0, dur, freqs, amp, attack=1.4, release=1.8, bright=0.25, detune=0.003, lp=None):
|
||||
"""A slow, warm chord: two detuned sines per note plus a little 2nd harmonic."""
|
||||
i0 = int(t0 * sr)
|
||||
n = int((dur + release) * sr)
|
||||
L = len(buf)
|
||||
voices = []
|
||||
for f in freqs:
|
||||
for d in (-detune, detune):
|
||||
voices.append([0.0, TAU * f * (1 + d) / sr, 0.0, TAU * 2 * f * (1 + d) / sr])
|
||||
a_n, d_n = attack * sr, dur * sr
|
||||
r_n = release * sr
|
||||
y = 0.0
|
||||
for j in range(n):
|
||||
if j < a_n:
|
||||
e = j / a_n
|
||||
elif j < d_n:
|
||||
e = 1.0
|
||||
else:
|
||||
e = max(0.0, 1.0 - (j - d_n) / r_n)
|
||||
e = e * e * (3 - 2 * e)
|
||||
s = 0.0
|
||||
for v in voices:
|
||||
s += sin(v[0]) + bright * sin(v[2])
|
||||
v[0] += v[1]
|
||||
v[2] += v[3]
|
||||
s *= amp * e / len(voices)
|
||||
if lp:
|
||||
y += lp * (s - y)
|
||||
s = y
|
||||
buf[(i0 + j) % L] += s
|
||||
|
||||
|
||||
def add_saw(buf, sr, t0, dur, freq, amp, lp=0.08, attack=0.01, release=0.15, trem=0.0, trem_hz=6.0):
|
||||
"""A filtered saw: the ostinato, low strings and brass."""
|
||||
i0 = int(t0 * sr)
|
||||
n = int((dur + release) * sr)
|
||||
L = len(buf)
|
||||
ph, inc = 0.0, freq / sr
|
||||
y1 = y2 = 0.0
|
||||
a_n, d_n, r_n = max(1, attack * sr), dur * sr, max(1, release * sr)
|
||||
for j in range(n):
|
||||
if j < a_n:
|
||||
e = j / a_n
|
||||
elif j < d_n:
|
||||
e = 1.0
|
||||
else:
|
||||
e = max(0.0, 1.0 - (j - d_n) / r_n)
|
||||
if trem:
|
||||
e *= 1.0 - trem * (0.5 + 0.5 * sin(TAU * trem_hz * j / sr))
|
||||
s = 2.0 * ph - 1.0
|
||||
ph += inc
|
||||
if ph >= 1.0:
|
||||
ph -= 1.0
|
||||
y1 += lp * (s - y1)
|
||||
y2 += lp * (y1 - y2)
|
||||
buf[(i0 + j) % L] += y2 * amp * e
|
||||
|
||||
|
||||
def add_drum(buf, sr, t0, f0, amp, drop=1.6, fall=28.0, decay=8.0, noise=0.15, dur=0.7, rng=None):
|
||||
"""Toms, kicks and timpani: a pitch-dropping sine with a noisy start."""
|
||||
i0 = int(t0 * sr)
|
||||
n = int(dur * sr)
|
||||
L = len(buf)
|
||||
ph = 0.0
|
||||
y = 0.0
|
||||
rng = rng or random.Random(int(t0 * 1000))
|
||||
for j in range(n):
|
||||
t = j / sr
|
||||
f = f0 * (1.0 + drop * exp(-t * fall))
|
||||
ph += TAU * f / sr
|
||||
s = sin(ph) * exp(-t * decay)
|
||||
y += 0.3 * ((rng.random() * 2 - 1) - y)
|
||||
s += noise * y * exp(-t * 30.0)
|
||||
buf[(i0 + j) % L] += s * amp
|
||||
|
||||
|
||||
def add_noise(buf, sr, t0, dur, amp, lp=0.2, hp=0.0, env=None, rng=None):
|
||||
"""Filtered noise with an envelope function env(k) for k in 0..1."""
|
||||
i0 = int(t0 * sr)
|
||||
n = int(dur * sr)
|
||||
L = len(buf)
|
||||
rng = rng or random.Random(int(t0 * 977) + 3)
|
||||
y = z = 0.0
|
||||
for j in range(n):
|
||||
x = rng.random() * 2 - 1
|
||||
y += lp * (x - y)
|
||||
s = y
|
||||
if hp:
|
||||
z += hp * (s - z)
|
||||
s = s - z
|
||||
e = env(j / n) if env else 1.0
|
||||
buf[(i0 + j) % L] += s * amp * e
|
||||
|
||||
|
||||
def echo(src, sr, delay, fb, mix, loops=2):
|
||||
"""A circular (loop-safe) feedback delay; returns the wet signal."""
|
||||
L = len(src)
|
||||
d = int(delay * sr)
|
||||
wet = [0.0] * L
|
||||
for _ in range(loops):
|
||||
for i in range(L):
|
||||
wet[i] = src[(i - d) % L] * mix + wet[(i - d) % L] * fb
|
||||
return wet
|
||||
|
||||
|
||||
def lowpass(src, a):
|
||||
y = 0.0
|
||||
out = [0.0] * len(src)
|
||||
for _ in range(2): # (twice round, so the loop point has no click)
|
||||
for i in range(len(src)):
|
||||
y += a * (src[i] - y)
|
||||
out[i] = y
|
||||
return out
|
||||
|
||||
|
||||
# --- the music ---------------------------------------------------------------------
|
||||
|
||||
def _mix(sr, L, dry_l, dry_r, send, d_l=0.37, d_r=0.29, fb=0.38, mix=0.5):
|
||||
wl = echo(send, sr, d_l, fb, mix)
|
||||
wr = echo(send, sr, d_r, fb, mix)
|
||||
return ([dry_l[i] + send[i] + wl[i] for i in range(L)],
|
||||
[dry_r[i] + send[i] + wr[i] for i in range(L)])
|
||||
|
||||
|
||||
def track_calm():
|
||||
"""Day: D major, 72 bpm, a warm pad, soft bass, and a sparse harp melody."""
|
||||
sr, bpm, bars = 22050, 72, 16
|
||||
beat = 60.0 / bpm
|
||||
L = int(bars * 4 * beat * sr)
|
||||
rng = random.Random(11)
|
||||
pad, bass, mel = [0.0] * L, [0.0] * L, [0.0] * L
|
||||
# D, Bm, G, A, D, F#m, G, A: two bars each.
|
||||
chords = [(62, 66, 69), (59, 62, 66), (55, 59, 62), (57, 61, 64),
|
||||
(62, 66, 69), (54, 57, 61), (55, 59, 62), (57, 61, 64)]
|
||||
roots = [38, 35, 43, 45, 38, 42, 43, 45]
|
||||
scale = [74, 76, 78, 81, 83, 86, 88] # D pentatonic, upper
|
||||
for c, ch in enumerate(chords):
|
||||
t = c * 8 * beat
|
||||
add_pad(pad, sr, t, 8 * beat, [midi(m) for m in ch], 0.55, attack=1.8, release=2.2, lp=0.18)
|
||||
add_pluck(bass, sr, t, midi(roots[c]), 0.30, decay=0.9, harm=(1.0, 0.25))
|
||||
add_pluck(bass, sr, t + 4 * beat, midi(roots[c]), 0.22, decay=0.9, harm=(1.0, 0.25))
|
||||
prev = 3
|
||||
for b in range(8):
|
||||
if (c % 2 == 1 and b >= 6) or rng.random() > 0.55:
|
||||
continue
|
||||
prev = max(0, min(len(scale) - 1, prev + rng.choice([-2, -1, -1, 1, 1, 2])))
|
||||
m = scale[prev]
|
||||
off = rng.choice([0.0, 0.0, 0.5]) * beat
|
||||
add_pluck(mel, sr, t + b * beat + off, midi(m), 0.26 + rng.random() * 0.08, decay=2.0)
|
||||
if rng.random() < 0.18:
|
||||
add_pluck(mel, sr, t + b * beat + off + 0.5 * beat, midi(m + 12), 0.1, decay=3.0,
|
||||
harm=(1.0, 0.1, 0.05))
|
||||
dry = [pad[i] + bass[i] for i in range(L)]
|
||||
return sr, _mix(sr, L, dry, dry, mel)
|
||||
|
||||
|
||||
def track_night():
|
||||
"""Night: A minor, 56 bpm, dark pad, a low drone and a few distant notes."""
|
||||
sr, bpm, bars = 22050, 56, 12
|
||||
beat = 60.0 / bpm
|
||||
L = int(bars * 4 * beat * sr)
|
||||
rng = random.Random(23)
|
||||
pad, low, mel = [0.0] * L, [0.0] * L, [0.0] * L
|
||||
chords = [(57, 60, 64), (53, 57, 60), (48, 52, 55), (55, 59, 62), (57, 60, 64), (52, 55, 59)]
|
||||
for c, ch in enumerate(chords):
|
||||
t = c * 8 * beat
|
||||
add_pad(pad, sr, t, 8 * beat, [midi(m) for m in ch], 0.5, attack=2.5, release=3.0, bright=0.1, lp=0.08)
|
||||
add_pad(low, sr, 0.0, L / sr - 3.0, [midi(33), midi(45)], 0.35, attack=3.0, release=3.0, bright=0.05, detune=0.001)
|
||||
scale = [69, 72, 74, 76, 79, 81] # A minor pentatonic
|
||||
t = 1.5 * beat
|
||||
while t < L / sr - 2.0:
|
||||
m = rng.choice(scale)
|
||||
add_pluck(mel, sr, t, midi(m), 0.18, decay=1.4, harm=(1.0, 0.2, 0.06))
|
||||
if rng.random() < 0.3:
|
||||
add_pluck(mel, sr, t + beat, midi(rng.choice(scale) - 12), 0.14, decay=1.2)
|
||||
t += beat * rng.choice([3, 4, 4, 5, 6])
|
||||
dry = [pad[i] + low[i] for i in range(L)]
|
||||
return sr, _mix(sr, L, dry, dry, mel, d_l=0.53, d_r=0.41, fb=0.45, mix=0.55)
|
||||
|
||||
|
||||
def track_storm():
|
||||
"""Storm, blizzard, the red moon: D minor, slow, tremolo low strings, rolls."""
|
||||
sr, bpm, bars = 22050, 60, 12
|
||||
beat = 60.0 / bpm
|
||||
L = int(bars * 4 * beat * sr)
|
||||
rng = random.Random(37)
|
||||
pad, strings, drums, wind = [0.0] * L, [0.0] * L, [0.0] * L, [0.0] * L
|
||||
chords = [(50, 53, 57), (46, 50, 53), (43, 46, 50), (45, 49, 52), (50, 53, 57), (46, 50, 55)]
|
||||
roots = [26, 22, 31, 33, 26, 22]
|
||||
for c, ch in enumerate(chords):
|
||||
t = c * 8 * beat
|
||||
add_pad(pad, sr, t, 8 * beat, [midi(m) for m in ch], 0.45, attack=2.0, release=2.0, bright=0.2, lp=0.12)
|
||||
add_saw(strings, sr, t, 8 * beat - 0.2, midi(roots[c] + 12), 0.5, lp=0.05, attack=1.0, release=1.0,
|
||||
trem=0.6, trem_hz=7.0)
|
||||
add_saw(strings, sr, t, 8 * beat - 0.2, midi(roots[c]), 0.35, lp=0.03, attack=1.5, release=1.0)
|
||||
# a timpani roll swelling into each second chord
|
||||
if c % 2 == 1:
|
||||
n = 22
|
||||
for k in range(n):
|
||||
tt = t + 6 * beat + k * (2 * beat / n)
|
||||
add_drum(drums, sr, tt, midi(roots[c] + 12) * 0.98, 0.1 + 0.5 * (k / n) ** 2, drop=0.2,
|
||||
fall=20, decay=6, noise=0.2, dur=0.5, rng=rng)
|
||||
add_drum(drums, sr, t, midi(roots[c] + 12), 0.8, drop=0.3, fall=18, decay=3.0, dur=1.6, rng=rng)
|
||||
add_noise(wind, sr, 0.0, L / sr, 0.25, lp=0.02,
|
||||
env=lambda k: 0.6 + 0.4 * sin(TAU * 3 * k) * sin(TAU * 5 * k + 1.0))
|
||||
dry = [pad[i] + strings[i] + drums[i] + wind[i] for i in range(L)]
|
||||
send = [0.25 * strings[i] for i in range(L)]
|
||||
return sr, _mix(sr, L, dry, dry, send, fb=0.3, mix=0.4)
|
||||
|
||||
|
||||
def _action(bpm, bars, seed, boss):
|
||||
sr = 22050
|
||||
beat = 60.0 / bpm
|
||||
L = int(bars * 4 * beat * sr)
|
||||
rng = random.Random(seed)
|
||||
pad, ost, drums, brass, fx = [0.0] * L, [0.0] * L, [0.0] * L, [0.0] * L, [0.0] * L
|
||||
# E minor: Em, C, D, B (with D#), then Em, Am, C, B.
|
||||
prog = [(40, (52, 55, 59)), (36, (48, 52, 55)), (38, (50, 54, 57)), (35, (47, 51, 54)),
|
||||
(40, (52, 55, 59)), (33, (45, 48, 52)), (36, (48, 52, 55)), (35, (47, 51, 54))]
|
||||
figure = [0, 0, 12, 0, 7, 0, 10, 7] if not boss else [0, 12, 0, 7, 0, 12, 10, 7]
|
||||
for bar in range(bars):
|
||||
root, ch = prog[bar % len(prog)]
|
||||
t = bar * 4 * beat
|
||||
build = bar >= bars - 4
|
||||
add_pad(pad, sr, t, 4 * beat, [midi(m) for m in ch], 0.35, attack=0.3, release=0.6, bright=0.3, lp=0.15)
|
||||
step = beat / 2
|
||||
for k in range(8):
|
||||
m = root + 12 + figure[k]
|
||||
add_saw(ost, sr, t + k * step, step * 0.7, midi(m), 0.42 if k % 2 == 0 else 0.3,
|
||||
lp=0.09 if boss else 0.07, attack=0.004, release=0.06)
|
||||
# drums: low tom on 1 and 3, a higher one on the "and" of 4, shaker 8ths
|
||||
add_drum(drums, sr, t, 70, 0.9, drop=1.2, decay=7, rng=rng)
|
||||
add_drum(drums, sr, t + 2 * beat, 70, 0.75, drop=1.2, decay=7, rng=rng)
|
||||
add_drum(drums, sr, t + 3.5 * beat, 105, 0.55, drop=1.0, decay=9, rng=rng)
|
||||
if boss:
|
||||
for b in range(4):
|
||||
add_drum(drums, sr, t + b * beat, 48, 0.8, drop=2.5, fall=35, decay=10, noise=0.05, rng=rng)
|
||||
if build or (boss and bar % 4 == 3):
|
||||
for k in range(8):
|
||||
add_drum(drums, sr, t + 2 * beat + k * beat / 4, 120 + k * 8, 0.25 + k * 0.05, decay=10, rng=rng)
|
||||
for k in range(8):
|
||||
add_noise(drums, sr, t + k * step, 0.06, 0.12 if k % 2 else 0.06, lp=0.9, hp=0.4,
|
||||
env=lambda q: exp(-q * 6), rng=rng)
|
||||
if boss:
|
||||
for m in ch:
|
||||
add_saw(brass, sr, t, 4 * beat - 0.1, midi(m), 0.22, lp=0.06, attack=0.25, release=0.3)
|
||||
if bar % 4 == 3:
|
||||
add_noise(fx, sr, t, 4 * beat, 0.35, lp=0.6, hp=0.2, env=lambda q: q ** 3, rng=rng)
|
||||
dry = [pad[i] + ost[i] + drums[i] + brass[i] + fx[i] for i in range(L)]
|
||||
send = [0.2 * ost[i] + 0.3 * brass[i] for i in range(L)]
|
||||
return sr, _mix(sr, L, dry, dry, send, d_l=beat * 0.75, d_r=beat * 0.5, fb=0.25, mix=0.35)
|
||||
|
||||
|
||||
def track_danger():
|
||||
"""Wolves at the fire, a bear, a fight: E minor, 104 bpm, ostinato and toms."""
|
||||
return _action(104, 20, 41, False)
|
||||
|
||||
|
||||
def track_boss():
|
||||
"""The Old Stag: the danger theme, faster, with brass, kicks and swells."""
|
||||
return _action(116, 24, 53, True)
|
||||
|
||||
|
||||
# --- ambience ---------------------------------------------------------------------
|
||||
|
||||
def amb_birds():
|
||||
sr, secs = 22050, 32
|
||||
L = sr * secs
|
||||
rng = random.Random(61)
|
||||
l, r = [0.0] * L, [0.0] * L
|
||||
add_noise(l, sr, 0, secs, 0.06, lp=0.03)
|
||||
add_noise(r, sr, 0, secs, 0.06, lp=0.03, rng=random.Random(5))
|
||||
t = 0.4
|
||||
while t < secs - 1.0:
|
||||
pan = rng.random()
|
||||
base = rng.uniform(2400, 4200)
|
||||
kind = rng.randrange(3)
|
||||
n = rng.randint(2, 6)
|
||||
tt = t
|
||||
for k in range(n):
|
||||
d = rng.uniform(0.05, 0.11)
|
||||
f0 = base * rng.uniform(0.9, 1.15)
|
||||
f1 = f0 * (rng.uniform(0.7, 0.85) if kind == 0 else rng.uniform(1.15, 1.35) if kind == 1 else 1.0)
|
||||
nn = int(d * sr)
|
||||
ph = 0.0
|
||||
a = rng.uniform(0.15, 0.3)
|
||||
for j in range(nn):
|
||||
q = j / nn
|
||||
ph += TAU * (f0 + (f1 - f0) * q) / sr
|
||||
e = sin(math.pi * q) ** 2
|
||||
s = sin(ph) * e * a
|
||||
i = (int(tt * sr) + j) % L
|
||||
l[i] += s * (1 - pan)
|
||||
r[i] += s * pan
|
||||
tt += d + rng.uniform(0.02, 0.09)
|
||||
t += rng.uniform(0.6, 2.6)
|
||||
return sr, (l, r)
|
||||
|
||||
|
||||
def amb_crickets():
|
||||
sr, secs = 22050, 30
|
||||
L = sr * secs
|
||||
rng = random.Random(71)
|
||||
l, r = [0.0] * L, [0.0] * L
|
||||
add_noise(l, sr, 0, secs, 0.04, lp=0.02)
|
||||
add_noise(r, sr, 0, secs, 0.04, lp=0.02, rng=random.Random(9))
|
||||
for c in range(4):
|
||||
pan = rng.random()
|
||||
f = rng.uniform(3800, 4700)
|
||||
period = rng.uniform(0.5, 0.9)
|
||||
pulses = rng.randint(3, 5)
|
||||
amp = rng.uniform(0.06, 0.14)
|
||||
t = rng.random() * period
|
||||
while t < secs:
|
||||
for p in range(pulses):
|
||||
nn = int(0.018 * sr)
|
||||
i0 = int((t + p * 0.032) * sr)
|
||||
for j in range(nn):
|
||||
e = sin(math.pi * j / nn)
|
||||
s = sin(TAU * f * j / sr) * e * amp
|
||||
l[(i0 + j) % L] += s * (1 - pan)
|
||||
r[(i0 + j) % L] += s * pan
|
||||
t += period * rng.uniform(0.95, 1.05)
|
||||
if rng.random() < 0.08:
|
||||
t += rng.uniform(1.0, 3.0) # (they pause now and then)
|
||||
return sr, (l, r)
|
||||
|
||||
|
||||
def amb_fire():
|
||||
sr, secs = 22050, 6
|
||||
L = sr * secs
|
||||
rng = random.Random(83)
|
||||
b = [0.0] * L
|
||||
add_noise(b, sr, 0, secs, 0.35, lp=0.015, env=lambda k: 0.8 + 0.2 * sin(TAU * 4 * k))
|
||||
add_noise(b, sr, 0, secs, 0.06, lp=0.3, hp=0.1, env=lambda k: 0.6 + 0.4 * sin(TAU * 7 * k + 2))
|
||||
t = 0.0
|
||||
while t < secs:
|
||||
a = rng.uniform(0.2, 0.9) if rng.random() < 0.85 else rng.uniform(1.0, 1.6)
|
||||
add_noise(b, sr, t, rng.uniform(0.004, 0.02), a, lp=0.8, hp=0.3, env=lambda q: exp(-q * 5), rng=rng)
|
||||
t += rng.expovariate(9.0)
|
||||
return sr, (b,)
|
||||
|
||||
|
||||
# --- sound effects ------------------------------------------------------------------
|
||||
|
||||
SR_FX = 44100
|
||||
|
||||
|
||||
def fx_buf(secs):
|
||||
return [0.0] * int(secs * SR_FX)
|
||||
|
||||
|
||||
def fx_whoosh(seed, secs=0.32, f0=0.04, f1=0.25, amp=1.0):
|
||||
rng = random.Random(seed)
|
||||
b = fx_buf(secs + 0.05)
|
||||
y = z = 0.0
|
||||
n = int(secs * SR_FX)
|
||||
for j in range(n):
|
||||
q = j / n
|
||||
a = f0 + (f1 - f0) * sin(math.pi * q)
|
||||
x = rng.random() * 2 - 1
|
||||
y += a * (x - y)
|
||||
z += 0.03 * (y - z)
|
||||
b[j] = (y - z) * sin(math.pi * q) ** 1.5 * amp
|
||||
return b
|
||||
|
||||
|
||||
def fx_crunch(seed):
|
||||
rng = random.Random(seed)
|
||||
b = fx_buf(0.5)
|
||||
t = 0.0
|
||||
for k in range(rng.randint(3, 4)):
|
||||
d = rng.uniform(0.05, 0.09)
|
||||
add_noise(b, SR_FX, t, d, rng.uniform(0.7, 1.0), lp=rng.uniform(0.35, 0.7), hp=0.05,
|
||||
env=lambda q: exp(-q * 7) * min(1.0, q * 40), rng=rng)
|
||||
for _ in range(6):
|
||||
add_noise(b, SR_FX, t + rng.uniform(0, d), 0.004, rng.uniform(0.4, 0.9), lp=0.9, hp=0.3,
|
||||
env=lambda q: 1 - q, rng=rng)
|
||||
t += d + rng.uniform(0.04, 0.07)
|
||||
return b
|
||||
|
||||
|
||||
def fx_splash(seed, big=1.0):
|
||||
rng = random.Random(seed)
|
||||
b = fx_buf(0.9)
|
||||
add_noise(b, SR_FX, 0.0, 0.7, 0.9 * big, lp=0.25, hp=0.02, env=lambda q: exp(-q * 5) * min(1, q * 60), rng=rng)
|
||||
add_noise(b, SR_FX, 0.0, 0.25, 0.5 * big, lp=0.7, hp=0.2, env=lambda q: exp(-q * 9), rng=rng)
|
||||
for _ in range(int(10 * big)):
|
||||
t = rng.uniform(0.05, 0.6)
|
||||
f = rng.uniform(380, 1100)
|
||||
nn = int(0.05 * SR_FX)
|
||||
i0 = int(t * SR_FX)
|
||||
ph = 0.0
|
||||
for j in range(nn):
|
||||
q = j / nn
|
||||
ph += TAU * f * (1 + 0.8 * q) / SR_FX
|
||||
if i0 + j < len(b):
|
||||
b[i0 + j] += sin(ph) * sin(math.pi * q) * 0.12
|
||||
return b
|
||||
|
||||
|
||||
def fx_plop():
|
||||
b = fx_buf(0.3)
|
||||
ph = 0.0
|
||||
n = int(0.16 * SR_FX)
|
||||
for j in range(n):
|
||||
q = j / n
|
||||
ph += TAU * (650 - 430 * q) / SR_FX
|
||||
b[j] = sin(ph) * exp(-q * 4) * min(1, q * 80) * 0.9
|
||||
add_noise(b, SR_FX, 0.0, 0.12, 0.25, lp=0.4, hp=0.1, env=lambda q: exp(-q * 8))
|
||||
return b
|
||||
|
||||
|
||||
def fx_reel():
|
||||
rng = random.Random(97)
|
||||
b = fx_buf(0.9)
|
||||
for k in range(16):
|
||||
t = k * 0.048 + rng.uniform(0, 0.004)
|
||||
add_noise(b, SR_FX, t, 0.006, 0.8, lp=0.9, hp=0.4, env=lambda q: 1 - q, rng=rng)
|
||||
i0 = int(t * SR_FX)
|
||||
for j in range(int(0.012 * SR_FX)):
|
||||
b[i0 + j] += sin(TAU * 2900 * j / SR_FX) * exp(-j / 90) * 0.35
|
||||
return b
|
||||
|
||||
|
||||
def fx_cast():
|
||||
b = fx_whoosh(101, secs=0.45, f0=0.05, f1=0.3)
|
||||
ph = 0.0
|
||||
n = int(0.6 * SR_FX)
|
||||
out = b + [0.0] * (n - len(b) + 10)
|
||||
for j in range(n):
|
||||
q = j / n
|
||||
ph += TAU * (2400 - 1200 * q) / SR_FX
|
||||
out[j] += sin(ph) * 0.05 * sin(math.pi * q)
|
||||
return out
|
||||
|
||||
|
||||
def fx_bow():
|
||||
b = fx_buf(0.6)
|
||||
add_noise(b, SR_FX, 0.0, 0.03, 0.6, lp=0.6, hp=0.2, env=lambda q: 1 - q)
|
||||
ph = [0.0, 0.0, 0.0]
|
||||
n = int(0.5 * SR_FX)
|
||||
for j in range(n):
|
||||
t = j / SR_FX
|
||||
f = 118 * (1 + 0.04 * sin(TAU * 18 * t) * exp(-t * 8))
|
||||
s = 0.0
|
||||
for k, h in enumerate((1.0, 0.5, 0.3)):
|
||||
ph[k] += TAU * f * (k + 1) / SR_FX
|
||||
s += h * sin(ph[k])
|
||||
b[j] += s * exp(-t * 9) * 0.6
|
||||
return b
|
||||
|
||||
|
||||
def fx_thump(seed):
|
||||
rng = random.Random(seed)
|
||||
b = fx_buf(0.45)
|
||||
ph = 0.0
|
||||
n = len(b)
|
||||
for j in range(n):
|
||||
t = j / SR_FX
|
||||
ph += TAU * (85 + 70 * exp(-t * 30)) / SR_FX
|
||||
b[j] = sin(ph) * exp(-t * 11) * 0.9
|
||||
add_noise(b, SR_FX, 0.0, 0.12, 0.4, lp=0.08, env=lambda q: exp(-q * 6), rng=rng)
|
||||
return b
|
||||
|
||||
|
||||
def fx_death():
|
||||
b = fx_buf(1.6)
|
||||
ph = [0.0, 0.0]
|
||||
n = len(b)
|
||||
for j in range(n):
|
||||
t = j / SR_FX
|
||||
ph[0] += TAU * (60 + 40 * exp(-t * 4)) / SR_FX
|
||||
ph[1] += TAU * (220 * 2 ** (-t * 1.2)) / SR_FX
|
||||
b[j] = sin(ph[0]) * exp(-t * 2.5) * 0.8 + sin(ph[1]) * exp(-t * 2.0) * 0.3 * min(1, t * 20)
|
||||
return b
|
||||
|
||||
|
||||
# --- creature voices -----------------------------------------------------------------
|
||||
# A buzzy source (a saw with a wandering pitch, roughened by fast amplitude
|
||||
# flutter and breath noise) through a few resonant "throat" filters.
|
||||
|
||||
def bandpass(src, sr, f, q, gain=1.0):
|
||||
w = TAU * f / sr
|
||||
alpha = sin(w) / (2 * q)
|
||||
cw = math.cos(w)
|
||||
a0 = 1 + alpha
|
||||
b0, b2 = alpha / a0, -alpha / a0
|
||||
a1, a2 = -2 * cw / a0, (1 - alpha) / a0
|
||||
out = [0.0] * len(src)
|
||||
x1 = x2 = y1 = y2 = 0.0
|
||||
for i, x in enumerate(src):
|
||||
y = b0 * x + b2 * x2 - a1 * y1 - a2 * y2
|
||||
x2, x1 = x1, x
|
||||
y2, y1 = y1, y
|
||||
out[i] = y * gain
|
||||
return out
|
||||
|
||||
|
||||
def voice(seed, dur, pitch, env, formants, breath=0.25, rasp=0.5, rasp_hz=28.0, jitter=0.04):
|
||||
"""pitch(q) and env(q) for q in 0..1; formants: [(Hz, Q, gain)]."""
|
||||
rng = random.Random(seed)
|
||||
sr = SR_FX
|
||||
n = int(dur * sr)
|
||||
src = [0.0] * n
|
||||
ph = 0.0
|
||||
wob = 0.0
|
||||
nz = 0.0
|
||||
for j in range(n):
|
||||
q = j / n
|
||||
wob += (rng.random() * 2 - 1) * 0.02 - wob * 0.002
|
||||
f = pitch(q) * (1 + jitter * wob)
|
||||
ph += f / sr
|
||||
ph -= int(ph)
|
||||
saw = 2 * ph - 1
|
||||
nz += 0.5 * ((rng.random() * 2 - 1) - nz)
|
||||
flutter = 1 - rasp * (0.5 + 0.5 * sin(TAU * rasp_hz * j / sr + 3 * nz))
|
||||
src[j] = (saw * flutter + breath * nz) * env(q)
|
||||
out = [0.0] * n
|
||||
for f, qq, g in formants:
|
||||
bp = bandpass(src, sr, f, qq, g)
|
||||
for j in range(n):
|
||||
out[j] += bp[j]
|
||||
return out
|
||||
|
||||
|
||||
def _swell(a, d, power=1.0):
|
||||
"""Rise over the first a of the sound, fall over the last d."""
|
||||
return lambda q: (min(1.0, q / a) * min(1.0, (1 - q) / d)) ** power
|
||||
|
||||
|
||||
def make_voices():
|
||||
d = os.path.join(OUT, "sfx", "creatures")
|
||||
print("creature voices ->", os.path.relpath(d, ROOT))
|
||||
W = [(420, 4, 1.0), (1150, 5, 0.6), (2500, 6, 0.25)] # a wolf's throat
|
||||
B = [(260, 3, 1.0), (720, 4, 0.7), (1700, 5, 0.25)] # a bear's, lower and wider
|
||||
for i in range(3):
|
||||
base = 72 + 9 * i
|
||||
_encode(os.path.join(d, "wolf_growl_%d.ogg" % i),
|
||||
[voice(100 + i, 1.1 + 0.2 * i, lambda q, b=base: b * (1 + 0.15 * sin(TAU * 1.3 * q)),
|
||||
_swell(0.25, 0.3), W, breath=0.45, rasp=0.75, rasp_hz=24 + 3 * i)], SR_FX, 5)
|
||||
for i in range(2):
|
||||
_encode(os.path.join(d, "wolf_snarl_%d.ogg" % i),
|
||||
[voice(110 + i, 0.45, lambda q, k=i: 150 + 90 * k + 160 * sin(math.pi * q),
|
||||
_swell(0.08, 0.4), W, breath=0.6, rasp=0.6, rasp_hz=38)], SR_FX, 5)
|
||||
_encode(os.path.join(d, "wolf_yelp_%d.ogg" % i),
|
||||
[voice(120 + i, 0.32, lambda q, k=i: 750 + 120 * k + 650 * sin(math.pi * min(1, q * 1.4)),
|
||||
_swell(0.05, 0.6), [(900, 3, 1.0), (2200, 4, 0.5)], breath=0.15, rasp=0.15, jitter=0.02)], SR_FX, 5)
|
||||
_encode(os.path.join(d, "bear_roar_0.ogg"),
|
||||
[voice(130, 1.7, lambda q: 58 + 30 * sin(math.pi * q ** 0.7), _swell(0.2, 0.35), B,
|
||||
breath=0.7, rasp=0.8, rasp_hz=21)], SR_FX, 5)
|
||||
_encode(os.path.join(d, "bear_roar_1.ogg"),
|
||||
[voice(131, 1.3, lambda q: 64 + 24 * sin(math.pi * q), _swell(0.12, 0.4), B,
|
||||
breath=0.75, rasp=0.85, rasp_hz=19)], SR_FX, 5)
|
||||
for i in range(2):
|
||||
_encode(os.path.join(d, "bear_grunt_%d.ogg" % i),
|
||||
[voice(140 + i, 0.45, lambda q, k=i: 70 + 12 * k - 15 * q, _swell(0.1, 0.5), B,
|
||||
breath=0.5, rasp=0.7, rasp_hz=17)], SR_FX, 5)
|
||||
P = [(380, 3, 1.0), (950, 4, 0.6), (2100, 5, 0.3)] # a boar's snout
|
||||
for i in range(2):
|
||||
pulses = [0.0, 0.16, 0.32][: 2 + i]
|
||||
def env(q, ps=pulses):
|
||||
t = q * 0.55
|
||||
return max(max(0.0, 1 - abs(t - p - 0.05) / 0.06) for p in ps)
|
||||
_encode(os.path.join(d, "boar_grunt_%d.ogg" % i),
|
||||
[voice(150 + i, 0.55, lambda q: 95 + 10 * sin(TAU * 3 * q), env, P, breath=0.6, rasp=0.6, rasp_hz=30)], SR_FX, 5)
|
||||
_encode(os.path.join(d, "boar_squeal_0.ogg"),
|
||||
[voice(160, 0.6, lambda q: 650 + 300 * sin(math.pi * q) + 40 * sin(TAU * 9 * q), _swell(0.08, 0.35),
|
||||
[(1200, 3, 1.0), (2600, 4, 0.5)], breath=0.3, rasp=0.35, rasp_hz=45)], SR_FX, 5)
|
||||
S = [(520, 4, 1.0), (1450, 5, 0.7), (2900, 6, 0.3)] # a stag's long neck
|
||||
for i in range(2):
|
||||
_encode(os.path.join(d, "stag_bellow_%d.ogg" % i),
|
||||
[voice(170 + i, 1.9 - 0.3 * i, lambda q, k=i: 105 + 8 * k + 55 * sin(math.pi * q ** 0.6), _swell(0.18, 0.3), S,
|
||||
breath=0.55, rasp=0.65, rasp_hz=26)], SR_FX, 5)
|
||||
sn = fx_buf(0.4)
|
||||
add_noise(sn, SR_FX, 0.0, 0.32, 1.0, lp=0.18, hp=0.02, env=lambda q: min(1, q * 25) * exp(-q * 5))
|
||||
_encode(os.path.join(d, "stag_snort_0.ogg"), [sn], SR_FX, 5)
|
||||
for i in range(2):
|
||||
rng = random.Random(180 + i)
|
||||
rb = fx_buf(0.6)
|
||||
t = 0.0
|
||||
while t < 0.5:
|
||||
i0 = int(t * SR_FX)
|
||||
f = rng.uniform(1300, 2300)
|
||||
for j in range(int(0.02 * SR_FX)):
|
||||
if i0 + j < len(rb):
|
||||
rb[i0 + j] += sin(TAU * f * j / SR_FX) * exp(-j / 130) * rng.uniform(0.5, 1.0)
|
||||
add_noise(rb, SR_FX, t, 0.006, 0.6, lp=0.9, hp=0.3, env=lambda q: 1 - q, rng=rng)
|
||||
t += rng.uniform(0.025, 0.06)
|
||||
_encode(os.path.join(d, "guard_rattle_%d.ogg" % i), [rb], SR_FX, 5)
|
||||
for i in range(2):
|
||||
caws = [0.0, 0.33][: 1 + i]
|
||||
def cenv(q, cs=caws, dur=0.32 + 0.33 * i):
|
||||
t = q * dur
|
||||
return max(max(0.0, min(1, (t - c) / 0.02)) * max(0.0, 1 - (t - c) / 0.24) if t >= c else 0.0 for c in cs)
|
||||
_encode(os.path.join(d, "crow_caw_%d.ogg" % i),
|
||||
[voice(190 + i, 0.32 + 0.33 * i, lambda q: 520 - 80 * q, cenv, [(1300, 3, 1.0), (2400, 4, 0.6)],
|
||||
breath=0.5, rasp=0.5, rasp_hz=60)], SR_FX, 5)
|
||||
for i in range(2):
|
||||
_encode(os.path.join(d, "fox_yip_%d.ogg" % i),
|
||||
[voice(200 + i, 0.18, lambda q, k=i: 800 + 80 * k + 500 * sin(math.pi * q), _swell(0.05, 0.5),
|
||||
[(1100, 3, 1.0), (2600, 4, 0.4)], breath=0.2, rasp=0.2, jitter=0.02)], SR_FX, 5)
|
||||
|
||||
|
||||
def make_sfx():
|
||||
d = os.path.join(OUT, "sfx", "synth")
|
||||
print("sound effects ->", os.path.relpath(d, ROOT))
|
||||
for i in range(3):
|
||||
_encode(os.path.join(d, "swing_%d.ogg" % i), [fx_whoosh(10 + i, secs=0.26 + 0.04 * i)], SR_FX, 5)
|
||||
_encode(os.path.join(d, "eat_%d.ogg" % i), [fx_crunch(20 + i)], SR_FX, 5)
|
||||
for i in range(2):
|
||||
_encode(os.path.join(d, "splash_%d.ogg" % i), [fx_splash(30 + i)], SR_FX, 5)
|
||||
_encode(os.path.join(d, "thump_%d.ogg" % i), [fx_thump(40 + i)], SR_FX, 5)
|
||||
_encode(os.path.join(d, "splash_big.ogg"), [fx_splash(35, 1.6)], SR_FX, 5)
|
||||
_encode(os.path.join(d, "plop.ogg"), [fx_plop()], SR_FX, 5)
|
||||
_encode(os.path.join(d, "reel.ogg"), [fx_reel()], SR_FX, 5)
|
||||
_encode(os.path.join(d, "cast.ogg"), [fx_cast()], SR_FX, 5)
|
||||
_encode(os.path.join(d, "bow.ogg"), [fx_bow()], SR_FX, 5)
|
||||
_encode(os.path.join(d, "death.ogg"), [fx_death()], SR_FX, 5)
|
||||
|
||||
|
||||
def make_ambience():
|
||||
d = os.path.join(OUT, "ambience")
|
||||
print("ambience ->", os.path.relpath(d, ROOT))
|
||||
for name, fn in (("birds", amb_birds), ("crickets", amb_crickets), ("fire", amb_fire)):
|
||||
sr, ch = fn()
|
||||
_encode(os.path.join(d, name + ".ogg"), list(ch), sr, 3)
|
||||
|
||||
|
||||
def make_music():
|
||||
d = os.path.join(OUT, "music")
|
||||
print("music ->", os.path.relpath(d, ROOT))
|
||||
for name, fn in (("calm", track_calm), ("night", track_night), ("storm", track_storm),
|
||||
("danger", track_danger), ("boss", track_boss)):
|
||||
sr, ch = fn()
|
||||
_encode(os.path.join(d, name + ".ogg"), list(ch), sr, 4)
|
||||
|
||||
|
||||
# --- the Kenney CC0 packs (copied once, renamed; see assets/audio/CREDITS.md) ----------
|
||||
|
||||
KENNEY = {
|
||||
# ours: (pack, file in its Audio/ folder)
|
||||
"chop": [("impact-sounds", "impactWood_medium_%03d.ogg" % i) for i in range(5)],
|
||||
"wood_heavy": [("impact-sounds", "impactWood_heavy_%03d.ogg" % i) for i in range(5)],
|
||||
"wood_light": [("impact-sounds", "impactWood_light_%03d.ogg" % i) for i in range(5)],
|
||||
"mine": [("impact-sounds", "impactMining_%03d.ogg" % i) for i in range(5)],
|
||||
"plank": [("impact-sounds", "impactPlank_medium_%03d.ogg" % i) for i in range(5)],
|
||||
"punch": [("impact-sounds", "impactPunch_medium_%03d.ogg" % i) for i in range(5)],
|
||||
"punch_heavy": [("impact-sounds", "impactPunch_heavy_%03d.ogg" % i) for i in range(5)],
|
||||
"soft_heavy": [("impact-sounds", "impactSoft_heavy_%03d.ogg" % i) for i in range(5)],
|
||||
"step_grass": [("impact-sounds", "footstep_grass_%03d.ogg" % i) for i in range(5)],
|
||||
"step_snow": [("impact-sounds", "footstep_snow_%03d.ogg" % i) for i in range(5)],
|
||||
"step_wood": [("impact-sounds", "footstep_wood_%03d.ogg" % i) for i in range(5)],
|
||||
"step_stone": [("impact-sounds", "footstep_concrete_%03d.ogg" % i) for i in range(5)],
|
||||
"creak": [("rpg-audio", "creak%d.ogg" % i) for i in (1, 2, 3)],
|
||||
"door_open": [("rpg-audio", "doorOpen_%d.ogg" % i) for i in (1, 2)],
|
||||
"door_close": [("rpg-audio", "doorClose_%d.ogg" % i) for i in (1, 2, 3, 4)],
|
||||
"cloth": [("rpg-audio", "cloth%d.ogg" % i) for i in (1, 2, 3, 4)],
|
||||
"leather": [("rpg-audio", n) for n in ("handleSmallLeather.ogg", "handleSmallLeather2.ogg", "dropLeather.ogg")],
|
||||
"knife": [("rpg-audio", n) for n in ("knifeSlice.ogg", "knifeSlice2.ogg")],
|
||||
"metal_click": [("rpg-audio", n) for n in ("metalClick.ogg", "metalLatch.ogg")],
|
||||
"ui_open": [("interface-sounds", "open_001.ogg")],
|
||||
"ui_close": [("interface-sounds", "close_001.ogg")],
|
||||
"ui_tick": [("interface-sounds", "tick_001.ogg")],
|
||||
"ui_good": [("interface-sounds", "confirmation_001.ogg")],
|
||||
"ui_step": [("interface-sounds", "confirmation_003.ogg")],
|
||||
"ui_catch": [("interface-sounds", "confirmation_002.ogg")],
|
||||
"ui_tip": [("interface-sounds", "question_001.ogg")],
|
||||
}
|
||||
|
||||
|
||||
def copy_kenney(src):
|
||||
d = os.path.join(OUT, "sfx", "kenney")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
by_name = {}
|
||||
for root, _, files in os.walk(src):
|
||||
pack = next((p for p in ("rpg-audio", "impact-sounds", "interface-sounds") if p in root), None)
|
||||
for f in files:
|
||||
if pack and f.endswith(".ogg"):
|
||||
by_name[(pack, f)] = os.path.join(root, f)
|
||||
n = 0
|
||||
for ours, lst in KENNEY.items():
|
||||
for i, key in enumerate(lst):
|
||||
if key not in by_name:
|
||||
sys.exit("missing from the Kenney packs: %s/%s" % key)
|
||||
shutil.copyfile(by_name[key], os.path.join(d, "%s_%d.ogg" % (ours, i)))
|
||||
n += 1
|
||||
print("copied %d Kenney sounds -> %s" % (n, os.path.relpath(d, ROOT)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
if "--kenney" in args:
|
||||
i = args.index("--kenney")
|
||||
copy_kenney(os.path.expanduser(args[i + 1]))
|
||||
del args[i:i + 2]
|
||||
what = args[0] if args else "all"
|
||||
if what in ("all", "sfx"):
|
||||
make_sfx()
|
||||
if what in ("all", "sfx", "voices"):
|
||||
make_voices()
|
||||
if what in ("all", "ambience"):
|
||||
make_ambience()
|
||||
if what in ("all", "music"):
|
||||
make_music()
|
||||
Reference in New Issue
Block a user