#!/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("= 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()