Files
smack-a-brother/tools/assets_generate.py
T
shweetandClaude Opus 5.5 2007fb7edc Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 01:30:33 -05:00

778 lines
27 KiB
Python

# Generates the prototype's synthesized sound effects into build/sfx-sets/synth (for the sound review), made with numpy
# and encoded to ogg through ffmpeg. The game's own sounds (resources/sfx) are built by tools/sfx_build.py, whose
# loudness targets (tools/sfx_loudness.json) were measured from these. The art is drawn by hand in resources/gfx.
import math
import pathlib
import subprocess
import tempfile
import wave
import numpy as np
ROOT = pathlib.Path(__file__).resolve().parent.parent
SFX = ROOT / "build" / "sfx-sets" / "synth"
RATE = 44100
def time_axis(seconds):
return np.arange(int(RATE * seconds)) / RATE
def envelope(t, attack, power):
length = t[-1] if len(t) else 1
rise = np.clip(t / max(attack, 1e-4), 0, 1)
fall = (1 - t / length) ** power
return rise * fall
def square(phase):
return np.sign(np.sin(phase))
def sweep_phase(t, start, end):
frequency = start + (end - start) * (t / t[-1])
return 2 * math.pi * np.cumsum(frequency) / RATE
def noise(t, seed):
return np.random.default_rng(seed).uniform(-1, 1, len(t))
def lowpass(signal, amount):
out = np.zeros_like(signal)
value = 0.0
for i, sample in enumerate(signal):
value += (sample - value) * amount
out[i] = value
return out
def lowpass_sweep(signal, start, end):
out = np.zeros_like(signal)
value = 0.0
amounts = np.linspace(start, end, len(signal))
for i, sample in enumerate(signal):
value += (sample - value) * amounts[i]
out[i] = value
return out
def sound_jump():
t = time_axis(0.14)
return 0.35 * square(sweep_phase(t, 260, 620)) * envelope(t, 0.005, 1.5)
def sound_land():
t = time_axis(0.1)
return 0.9 * lowpass(noise(t, 1), 0.08) * envelope(t, 0.002, 2) + 0.4 * square(sweep_phase(t, 110, 50)) * envelope(t, 0.002, 3)
def sound_step():
t = time_axis(0.04)
return 0.6 * lowpass(noise(t, 2), 0.2) * envelope(t, 0.001, 3)
def sound_whoosh(seconds, start, end, seed):
t = time_axis(seconds)
return 1.2 * lowpass_sweep(noise(t, seed), start, end) * np.sin(math.pi * t / t[-1])
def sound_poke():
t = time_axis(0.12)
return 0.8 * lowpass_sweep(noise(t, 5), 0.6, 0.05) * envelope(t, 0.002, 2) + 0.2 * square(sweep_phase(t, 700, 300)) * envelope(t, 0.002, 4)
def sound_blast():
t = time_axis(0.12)
return 0.35 * square(sweep_phase(t, 900, 220)) * envelope(t, 0.002, 1.5)
def sound_beam():
t = time_axis(0.45)
wobble = 2 * math.pi * np.cumsum(1200 + 300 * np.sin(2 * math.pi * 30 * t)) / RATE
return 0.3 * (square(wobble) * 0.6 + noise(t, 6) * 0.4) * envelope(t, 0.005, 1.2)
def sound_boomerang():
t = time_axis(0.3)
flutter = 0.5 + 0.5 * np.sin(2 * math.pi * 24 * t)
return 1.0 * lowpass(noise(t, 7), 0.15) * flutter * np.sin(math.pi * t / t[-1])
def sound_slam():
t = time_axis(0.6)
boom = np.sin(sweep_phase(t, 120, 30)) * envelope(t, 0.002, 2)
rumble = lowpass(noise(t, 9), 0.05) * envelope(t, 0.002, 1.5)
return 0.8 * boom + 1.5 * rumble
def sound_charge():
t = time_axis(0.8)
tone = np.sin(sweep_phase(t, 220, 440)) + 0.3 * np.sin(sweep_phase(t, 440, 880))
shimmer = 0.75 + 0.25 * np.sin(2 * math.pi * 12 * t)
return 0.12 * tone * shimmer * np.clip(t / 0.15, 0, 1)
def sound_ready():
t = time_axis(0.5)
bell = np.sin(2 * math.pi * 660 * t) + 0.6 * np.sin(2 * math.pi * 990 * t) + 0.2 * np.sin(2 * math.pi * 1320 * t)
return 0.16 * bell * envelope(t, 0.005, 3)
def sound_arpeggio(frequencies):
t = time_axis(0.36)
step = len(t) // len(frequencies)
frequency = np.array([frequencies[min(i // step, len(frequencies) - 1)] for i in range(len(t))])
phase = 2 * math.pi * np.cumsum(frequency) / RATE
return 0.3 * square(phase) * envelope(t, 0.002, 1.2)
def sound_twang():
t = time_axis(0.25)
string = np.sin(sweep_phase(t, 260, 200)) + 0.5 * np.sin(sweep_phase(t, 520, 400))
click = lowpass(noise(t, 11), 0.4) * envelope(t, 0.001, 12)
return 0.4 * string * envelope(t, 0.002, 3) + 0.5 * click
def sound_rocket():
t = time_axis(0.4)
thump = np.sin(sweep_phase(t, 90, 40)) * envelope(t, 0.002, 6)
hiss = lowpass_sweep(noise(t, 12), 0.05, 0.5) * envelope(t, 0.03, 1.5)
return 0.6 * thump + 0.8 * hiss
def sound_grapple():
t = time_axis(0.16)
zip_tone = square(sweep_phase(t, 400, 1300)) * envelope(t, 0.003, 1.5)
hiss = lowpass_sweep(noise(t, 13), 0.1, 0.7) * envelope(t, 0.003, 1.5)
return 0.2 * zip_tone + 0.5 * hiss
def sound_latch():
t = time_axis(0.18)
ring = np.sin(2 * math.pi * 1800 * t) + 0.7 * np.sin(2 * math.pi * 2650 * t)
click = lowpass(noise(t, 14), 0.6) * envelope(t, 0.001, 10)
return 0.25 * ring * envelope(t, 0.001, 4) + 0.6 * click
def sound_explosion():
t = time_axis(0.9)
rumble = lowpass(noise(t, 15), 0.03) * envelope(t, 0.005, 1.3)
crackle = (np.random.default_rng(16).random(len(t)) > 0.995) * envelope(t, 0.01, 2)
boom = np.sin(sweep_phase(t, 70, 25)) * envelope(t, 0.002, 2.5)
return 2.2 * rumble + 0.6 * crackle + 0.6 * boom
def sound_flail():
t = time_axis(0.3)
rattle = 0.5 + 0.5 * np.sign(np.sin(2 * math.pi * 38 * t))
whoosh = lowpass_sweep(noise(t, 17), 0.05, 0.35) * np.sin(math.pi * t / t[-1])
return 1.1 * whoosh * (0.4 + 0.6 * rattle)
def sound_flail_land():
t = time_axis(0.22)
thud = np.sin(sweep_phase(t, 90, 45)) * envelope(t, 0.001, 4)
clank = np.sin(2 * math.pi * 900 * t) * envelope(t, 0.001, 8)
return 0.8 * thud + 0.25 * clank
def sound_dash():
t = time_axis(0.14)
air = lowpass_sweep(noise(t, 18), 0.6, 0.15) * envelope(t, 0.002, 1.5)
chirp = np.sin(sweep_phase(t, 600, 900)) * envelope(t, 0.002, 3)
return 0.8 * air + 0.15 * chirp
def sound_climb():
t = time_axis(0.12)
scrape = lowpass(noise(t, 19), 0.35) * envelope(t, 0.001, 2)
blip = square(sweep_phase(t, 300, 520)) * envelope(t, 0.001, 2.5)
return 0.6 * scrape + 0.25 * blip
def sound_echo():
t = time_axis(0.28)
shimmer = np.sin(2 * math.pi * 1400 * t) * (0.6 + 0.4 * np.sin(2 * math.pi * 22 * t))
return 0.25 * shimmer * envelope(t, 0.005, 2)
def place(signal, part, seconds):
start = int(seconds * RATE)
signal[start : start + len(part)] += part[: max(0, len(signal) - start)]
def part_click(seed):
t = time_axis(0.012)
return lowpass(noise(t, seed), 0.8) * envelope(t, 0.0005, 6)
def part_slide(rising, seed):
t = time_axis(0.07)
start, end = (0.08, 0.5) if rising else (0.5, 0.08)
return 0.6 * lowpass_sweep(noise(t, seed), start, end) * np.sin(math.pi * t / t[-1])
def part_clack():
t = time_axis(0.12)
thump = np.sin(sweep_phase(t, 150, 70)) * envelope(t, 0.001, 5)
metal = (np.sin(2 * math.pi * 1700 * t) + 0.6 * np.sin(2 * math.pi * 2450 * t)) * envelope(t, 0.001, 9)
return 0.7 * thump + 0.25 * metal
def sound_equip():
signal = np.zeros(int(RATE * 0.26))
place(signal, part_click(22), 0)
place(signal, part_slide(True, 23), 0.025)
place(signal, part_clack(), 0.11)
place(signal, 0.7 * part_click(24), 0.11)
return signal
def sound_unequip():
signal = np.zeros(int(RATE * 0.26))
place(signal, 0.6 * part_clack(), 0)
place(signal, part_slide(False, 25), 0.06)
place(signal, 0.8 * part_click(26), 0.15)
return signal
def sound_swap():
t = time_axis(0.09)
ticks = np.zeros_like(t)
ticks[: len(t) // 6] = 1
ticks[len(t) // 2 : len(t) // 2 + len(t) // 6] = 1
return 0.5 * lowpass(noise(t, 20), 0.5) * ticks
def sound_ricochet():
t = time_axis(0.2)
ping = np.sin(sweep_phase(t, 2600, 1500)) * envelope(t, 0.001, 3)
click = lowpass(noise(t, 27), 0.7) * envelope(t, 0.0005, 14)
return 0.3 * ping + 0.5 * click
def sound_pound():
t = time_axis(0.55)
thud = np.sin(sweep_phase(t, 60, 30)) * envelope(t, 0.002, 2.5)
crunch = lowpass(noise(t, 28), 0.2) * envelope(t, 0.002, 5)
clang = np.sin(2 * math.pi * 420 * t) * envelope(t, 0.001, 7)
return 1.0 * thud + 0.9 * crunch + 0.2 * clang
def sound_zap():
t = time_axis(0.12)
buzz = square(2 * math.pi * 90 * t) * (np.random.default_rng(29).random(len(t)) > 0.4)
crackle = lowpass(noise(t, 30), 0.9) * envelope(t, 0.001, 3)
return (0.25 * buzz + 0.4 * crackle) * envelope(t, 0.002, 2)
def sound_uzi():
t = time_axis(0.06)
pop = square(sweep_phase(t, 1400, 500)) * envelope(t, 0.0005, 3)
crack = lowpass(noise(t, 31), 0.8) * envelope(t, 0.0005, 6)
return 0.25 * pop + 0.45 * crack
def sound_mine():
t = time_axis(0.25)
clunk = np.sin(sweep_phase(t, 220, 120)) * envelope(t, 0.001, 8) + lowpass(noise(t, 32), 0.5) * envelope(t, 0.001, 14)
beep = square(2 * math.pi * 1600 * t) * ((t > 0.12) & (t < 0.17))
return 0.6 * clunk + 0.15 * beep
def sound_detonate():
t = time_axis(0.12)
gate = (t < 0.04) | ((t > 0.07) & (t < 0.11))
return 0.2 * square(2 * math.pi * 2200 * t) * gate
def sound_flame():
t = time_axis(0.12)
roar = lowpass_sweep(noise(t, 33), 0.08, 0.2) * np.sin(math.pi * t / t[-1])
crackle = (np.random.default_rng(34).random(len(t)) > 0.985) * envelope(t, 0.002, 2)
return 0.6 * roar + 0.15 * crackle
def sound_fireball():
t = time_axis(0.5)
whoomp = np.sin(sweep_phase(t, 140, 60)) * envelope(t, 0.01, 3)
roar = lowpass_sweep(noise(t, 35), 0.3, 0.04) * envelope(t, 0.02, 2)
return 0.6 * whoomp + 1.2 * roar
def highpass(signal, amount):
return signal - lowpass(signal, amount)
def sound_whip():
t = time_axis(0.2)
swish = lowpass_sweep(noise(t, 36), 0.02, 0.3) * np.clip(t / 0.04, 0, 1) * (t < 0.04)
crack = highpass(noise(t, 37), 0.3) * envelope(np.maximum(t - 0.04, 0), 0.0002, 40) * (t >= 0.04)
snap = square(sweep_phase(t, 3000, 500)) * envelope(np.maximum(t - 0.04, 0), 0.0003, 30) * (t >= 0.04)
thump = np.sin(sweep_phase(t, 180, 60)) * envelope(np.maximum(t - 0.04, 0), 0.001, 12) * (t >= 0.04)
return 0.95 * np.tanh(3 * (0.3 * swish + 1.4 * crack + 0.35 * snap + 0.9 * thump))
def sound_whip_blast():
t = time_axis(0.5)
crack = highpass(noise(t, 38), 0.3) * envelope(t, 0.0002, 25)
boom = np.sin(sweep_phase(t, 130, 35)) * envelope(t, 0.001, 4)
rumble = lowpass(noise(t, 39), 0.06) * envelope(t, 0.002, 5)
return 0.95 * np.tanh(4 * (1.2 * crack + 1.0 * boom + 1.6 * rumble))
def sound_clack(t, start, pitch, seed):
local = np.maximum(t - start, 0)
gate = (t >= start) & (local < 0.05)
rattle = highpass(noise(t, seed), 0.4) * (1 - local / 0.05) ** 4
ring = np.sin(2 * math.pi * pitch * local) * (1 - local / 0.05) ** 6
thunk = np.sin(2 * math.pi * 220 * local) * (1 - local / 0.05) ** 3
return gate * (0.9 * rattle + 0.35 * ring + 0.5 * thunk)
def sound_shotgun():
t = time_axis(0.7)
boom_t = t[t < 0.45]
blast = highpass(noise(boom_t, 40), 0.15) * envelope(boom_t, 0.0002, 14)
body = lowpass(noise(boom_t, 41), 0.12) * envelope(boom_t, 0.001, 5)
boom = np.sin(sweep_phase(boom_t, 110, 40)) * envelope(boom_t, 0.001, 6)
shot = np.zeros_like(t)
shot[: len(boom_t)] = 0.95 * np.tanh(4 * (1.0 * blast + 1.4 * body + 1.0 * boom))
pump = sound_clack(t, 0.42, 1500, 42) + sound_clack(t, 0.56, 2100, 43)
return shot + 0.6 * pump
def sound_death():
t = time_axis(0.7)
boom = np.sin(sweep_phase(t, 220, 40)) * envelope(t, 0.001, 3)
crunch = lowpass_sweep(noise(t, 44), 0.5, 0.03) * envelope(t, 0.001, 2.5)
chime = np.sin(sweep_phase(t, 880, 220)) * envelope(t, 0.002, 4)
return 0.95 * np.tanh(3 * (0.9 * boom + 1.2 * crunch + 0.3 * chime))
def sound_bounce():
t = time_axis(0.22)
thud = np.sin(sweep_phase(t, 170, 60)) * envelope(t, 0.001, 5)
smack = lowpass(noise(t, 45), 0.4) * envelope(t, 0.0005, 14)
return 0.95 * np.tanh(2.5 * (1.0 * thud + 0.8 * smack))
def sound_detonate_empty():
t = time_axis(0.05)
click = highpass(noise(t, 46), 0.5) * envelope(t, 0.0003, 20)
tick = np.sin(2 * math.pi * 1400 * t) * envelope(t, 0.0005, 12)
return 0.12 * click + 0.06 * tick
def sound_slash_heavy():
t = time_axis(0.4)
whoosh = lowpass_sweep(noise(t, 50), 0.03, 0.25) * np.sin(math.pi * t / t[-1])
shing = (np.sin(2 * math.pi * 900 * t) + 0.5 * np.sin(2 * math.pi * 1350 * t)) * envelope(t, 0.01, 6)
thump = np.sin(sweep_phase(t, 130, 50)) * envelope(t, 0.002, 6)
return 0.95 * np.tanh(1.6 * (1.3 * whoosh + 0.15 * shing + 0.8 * thump))
def sound_swing_heavy():
t = time_axis(0.55)
whoosh = lowpass_sweep(noise(t, 51), 0.01, 0.12) * np.sin(math.pi * t / t[-1]) ** 0.7
growl = np.sin(sweep_phase(t, 55, 38)) * (0.5 + 0.5 * np.sin(2 * math.pi * 9 * t)) * envelope(t, 0.05, 2)
return 0.95 * np.tanh(2 * (1.6 * whoosh + 0.7 * growl))
def sound_flail_heavy():
t = time_axis(0.45)
rattle = 0.5 + 0.5 * np.sign(np.sin(2 * math.pi * 16 * t))
swirl = lowpass_sweep(noise(t, 52), 0.02, 0.15) * np.sin(math.pi * t / t[-1])
clunk = np.sin(sweep_phase(t, 160, 70)) * envelope(t, 0.001, 10)
return 0.95 * np.tanh(1.8 * (1.3 * swirl * (0.4 + 0.6 * rattle) + 0.7 * clunk))
def sound_poke_heavy():
t = time_axis(0.35)
rush = lowpass_sweep(noise(t, 53), 0.25, 0.02) * envelope(t, 0.01, 1.5)
boom = np.sin(sweep_phase(t, 120, 45)) * envelope(t, 0.002, 4)
edge = square(sweep_phase(t, 420, 160)) * envelope(t, 0.002, 6)
return 0.95 * np.tanh(1.8 * (1.2 * rush + 0.9 * boom + 0.12 * edge))
def sound_whip_heavy():
t = time_axis(0.35)
hit = 0.1
swish = lowpass_sweep(noise(t, 54), 0.01, 0.25) * np.clip(t / hit, 0, 1) * (t < hit)
after = np.maximum(t - hit, 0)
crack = highpass(noise(t, 55), 0.25) * envelope(after, 0.0002, 30) * (t >= hit)
boom = np.sin(sweep_phase(t, 110, 35)) * envelope(after, 0.001, 5) * (t >= hit)
return 0.95 * np.tanh(3 * (0.5 * swish + 1.1 * crack + 1.2 * boom))
def sound_grapple_heavy():
t = time_axis(0.25)
zing = square(sweep_phase(t, 180, 600)) * envelope(t, 0.003, 2)
cable = lowpass_sweep(noise(t, 56), 0.05, 0.4) * envelope(t, 0.003, 2)
thunk = np.sin(sweep_phase(t, 140, 55)) * envelope(np.maximum(t - 0.12, 0), 0.001, 6) * (t >= 0.12)
return 0.95 * np.tanh(1.8 * (0.25 * zing + 0.6 * cable + 1.0 * thunk))
def sound_boomerang_heavy():
t = time_axis(0.5)
flutter = 0.5 + 0.5 * np.sin(2 * math.pi * 13 * t)
whoosh = lowpass(noise(t, 57), 0.06) * flutter * np.sin(math.pi * t / t[-1])
hum = np.sin(2 * math.pi * 70 * t) * flutter * np.sin(math.pi * t / t[-1])
return 0.95 * np.tanh(2 * (1.4 * whoosh + 0.3 * hum))
def sound_bow_heavy():
t = time_axis(0.45)
string = np.sin(sweep_phase(t, 150, 105)) + 0.6 * np.sin(sweep_phase(t, 300, 210)) + 0.3 * np.sin(sweep_phase(t, 450, 315))
thwack = lowpass(noise(t, 58), 0.3) * envelope(t, 0.0005, 14)
return 0.95 * np.tanh(1.6 * (0.5 * string * envelope(t, 0.002, 2) + 0.9 * thwack))
def sound_uzi_heavy():
t = time_axis(0.2)
burst = np.zeros_like(t)
for shot in range(5):
start = shot * 0.03
after = np.maximum(t - start, 0)
gate = (t >= start) & (after < 0.05)
pop = square(2 * math.pi * (260 - 40 * shot) * after) * (1 - after / 0.05) ** 3
crack = lowpass(noise(t, 59 + shot), 0.5) * (1 - after / 0.05) ** 5
burst += gate * (0.4 * pop + 0.6 * crack)
thump = np.sin(sweep_phase(t, 100, 45)) * envelope(t, 0.001, 3)
return 0.95 * np.tanh(2 * (burst + 0.6 * thump))
def sound_shotgun_heavy():
t = time_axis(0.85)
boom_t = t[t < 0.6]
blast = highpass(noise(boom_t, 64), 0.2) * envelope(boom_t, 0.0003, 16)
body = lowpass(noise(boom_t, 65), 0.07) * envelope(boom_t, 0.001, 4)
boom = np.sin(sweep_phase(boom_t, 80, 28)) * envelope(boom_t, 0.001, 4)
shot = np.zeros_like(t)
shot[: len(boom_t)] = 0.95 * np.tanh(4 * (0.7 * blast + 1.6 * body + 1.3 * boom))
pump = sound_clack(t, 0.55, 1200, 66) + sound_clack(t, 0.7, 1700, 67)
return shot + 0.6 * pump
def sound_rocket_heavy():
t = time_axis(0.8)
ignite = np.sin(sweep_phase(t, 65, 22)) * envelope(t, 0.002, 4)
roar = lowpass_sweep(noise(t, 68), 0.02, 0.25) * np.clip(t / 0.15, 0, 1) * (1 - t / t[-1]) ** 1.2
return 0.95 * np.tanh(2.2 * (1.0 * ignite + 1.3 * roar))
def sound_break():
t = time_axis(0.35)
crunch = lowpass(noise(t, 70), 0.4) * envelope(t, 0.001, 5)
rubble = (np.random.default_rng(71).random(len(t)) > 0.97) * envelope(t, 0.01, 2) * 0.6
thump = np.sin(sweep_phase(t, 150, 50)) * envelope(t, 0.001, 6)
return 0.9 * np.tanh(1.8 * (1.0 * crunch + rubble + 0.9 * thump))
def sound_fall():
t = time_axis(0.45)
whistle = np.sin(sweep_phase(t, 1300, 250)) * envelope(t, 0.01, 1.5)
poof = lowpass(noise(t, 72), 0.15) * envelope(np.maximum(t - 0.3, 0), 0.005, 4) * (t >= 0.3)
return 0.35 * whistle + 0.9 * poof
def sound_finish():
notes = [523, 659, 784, 1047, 1319]
t = time_axis(0.9)
signal = np.zeros_like(t)
for i, frequency in enumerate(notes):
start = i * 0.08
after = np.maximum(t - start, 0)
signal += (t >= start) * (square(2 * math.pi * frequency * after) * 0.5 + np.sin(2 * math.pi * frequency * 2 * after) * 0.5) * envelope(after, 0.003, 3)
return 0.22 * signal
def sound_duck():
t = time_axis(0.14)
swish = lowpass_sweep(noise(t, 80), 0.25, 0.04) * np.sin(math.pi * t / t[-1])
thump = np.sin(sweep_phase(t, 160, 90)) * envelope(np.maximum(t - 0.08, 0), 0.001, 6) * (t >= 0.08)
return 0.45 * swish + 0.35 * thump
def sound_unduck():
t = time_axis(0.1)
return 0.35 * lowpass_sweep(noise(t, 81), 0.05, 0.3) * np.sin(math.pi * t / t[-1])
def sound_crawl():
t = time_axis(0.06)
return 0.6 * lowpass(noise(t, 82), 0.08) * np.sin(math.pi * t / t[-1])
def sound_roll():
t = time_axis(0.09)
bump = np.sin(sweep_phase(t, 130, 70)) * envelope(t, 0.002, 5)
scrape = lowpass(noise(t, 83), 0.1) * envelope(t, 0.002, 3)
return 0.5 * bump + 0.3 * scrape
def sound_impact_blaster():
t = time_axis(0.12)
pop = np.sin(sweep_phase(t, 1100, 300)) * envelope(t, 0.0005, 4)
fizz = highpass(noise(t, 84), 0.5) * envelope(t, 0.001, 3)
return 0.35 * pop + 0.3 * fizz
def sound_impact_bullet():
t = time_axis(0.07)
tick = square(sweep_phase(t, 2200, 900)) * envelope(t, 0.0003, 6)
chip = lowpass(noise(t, 85), 0.7) * envelope(t, 0.0003, 5)
return 0.15 * tick + 0.4 * chip
def sound_impact_arrow():
t = time_axis(0.2)
thunk = np.sin(sweep_phase(t, 260, 140)) * envelope(t, 0.0005, 6)
knock = lowpass(noise(t, 86), 0.3) * envelope(t, 0.0005, 8)
quiver = np.sin(2 * math.pi * 180 * t) * np.sin(2 * math.pi * 22 * t) * envelope(t, 0.01, 2)
return 0.6 * thunk + 0.5 * knock + 0.15 * quiver
def sound_impact_boomerang():
t = time_axis(0.15)
clonk = np.sin(2 * math.pi * 520 * t) + 0.6 * np.sin(2 * math.pi * 780 * t)
knock = lowpass(noise(t, 87), 0.4) * envelope(t, 0.0005, 10)
return 0.3 * clonk * envelope(t, 0.0005, 5) + 0.4 * knock
def sound_impact_mine():
t = time_axis(0.12)
return 0.6 * sound_clack(t, 0, 1300, 89)
def sound_punch():
t = time_axis(0.09)
whiff = lowpass_sweep(noise(t, 61), 0.5, 0.08) * envelope(t, 0.002, 3)
thud = np.sin(sweep_phase(t, 220, 90)) * envelope(t, 0.002, 5)
return 0.7 * whiff + 0.6 * thud
def sound_uppercut():
t = time_axis(0.45)
rise = lowpass_sweep(noise(t, 62), 0.04, 0.4) * np.sin(math.pi * t / t[-1]) ** 0.6
roar = np.sin(sweep_phase(t, 90, 260)) * envelope(t, 0.01, 2.5)
return 0.95 * np.tanh(1.8 * (1.2 * rise + 0.6 * roar))
def mix(signal, heavy):
peak = max(1e-6, np.max(np.abs(signal)))
return (0.95 if heavy else 0.75) * signal / peak
def thump(t, start, end, power):
return np.sin(sweep_phase(t, start, end)) * envelope(t, 0.001, power)
def ring(t, frequencies, power):
return sum(np.sin(2 * math.pi * f * t) / (i + 1) for i, f in enumerate(frequencies)) * envelope(t, 0.001, power)
def burst(t, seed, amount, power):
return lowpass(noise(t, seed), amount) * envelope(t, 0.0005, power)
# Hit sounds by hit type, each a light and a heavy (h = 1) take on the type's character.
def hit_blunt(h):
t = time_axis(0.18 + 0.2 * h)
return mix(thump(t, 300 - 120 * h, 110 - 60 * h, 3) + 0.7 * burst(t, 71, 0.15, 5) + h * 0.5 * burst(t, 72, 0.5, 9), h)
def hit_slash(h):
t = time_axis(0.16 + 0.14 * h)
return mix(ring(t, [2400 - 800 * h, 3600 - 1200 * h, 5100], 5) + 0.8 * burst(t, 70, 0.6, 8) + h * thump(t, 140, 60, 4), h)
def hit_pierce(h):
t = time_axis(0.1 + 0.12 * h)
pierce = highpass(noise(t, 74), 0.2) * envelope(t, 0.0005, 10)
return mix(pierce + 0.6 * np.sin(sweep_phase(t, 1300, 500)) * envelope(t, 0.001, 5) + h * thump(t, 150, 60, 4), h)
def hit_lash(h):
t = time_axis(0.08 + 0.1 * h)
crack = highpass(noise(t, 75), 0.35) * envelope(t, 0.0003, 14)
return mix(crack + 0.35 * square(sweep_phase(t, 2200, 700)) * envelope(t, 0.001, 8) + h * 0.7 * thump(t, 180, 70, 5), h)
def hit_bullet(h):
if h:
return hit_shotgun_heavy()
t = time_axis(0.05)
tick = burst(t, 80, 0.8, 12) + 0.5 * np.sin(2 * math.pi * 1100 * t) * envelope(t, 0.0005, 10)
return mix(tick, h)
def hit_energy(h):
t = time_axis(0.12 + 0.16 * h)
zap = square(sweep_phase(t, 1800 - 600 * h, 300 - 200 * h)) * envelope(t, 0.001, 3)
return mix(0.6 * zap + 0.4 * burst(t, 79, 0.7, 8) + h * 0.6 * thump(t, 120, 50, 3), h)
def hit_fire(h):
t = time_axis(0.22 + 0.25 * h)
hiss = highpass(noise(t, 82), 0.1) * envelope(t, 0.01, 2)
crackle = (np.random.default_rng(83).uniform(0, 1, len(t)) > 0.996) * envelope(t, 0.001, 1.5)
return mix(0.6 * hiss + 0.9 * crackle + h * 0.6 * burst(t, 84, 0.06, 2), h)
def hit_explosive(h):
t = time_axis(0.25 + 0.25 * h)
return mix(burst(t, 85, 0.08 - 0.03 * h, 2.5) + 0.9 * thump(t, 90 - 25 * h, 40 - 15 * h, 2.5), h)
def hit_magic(h):
t = time_axis(0.14 + 0.16 * h)
shimmer = ring(t, [1320 - 300 * h, 1980 - 450 * h, 2640], 6) * (1 + 0.4 * np.sin(2 * math.pi * 30 * t))
return mix(shimmer + 0.6 * burst(t, 90, 0.3, 7) + h * thump(t, 180, 70, 3), h)
# A shotgun blast's heavy thump, for heavy bullet hits.
def hit_shotgun_heavy():
t = time_axis(0.34)
return mix(burst(t, 81, 0.15, 4) + thump(t, 90, 35, 3), 1)
# The Grimoire's spells: wisps appearing, a wisp setting off, the teleport gathering, and the arrival pop.
def sound_cast():
t = time_axis(0.3)
chime = sum(np.sin(2 * math.pi * f * t) * envelope(t - d, 0.002, 4) * (t >= d) for f, d in [(880, 0), (1320, 0.05), (1760, 0.1)])
return 0.35 * chime
def sound_wisp():
t = time_axis(0.12)
return 0.3 * np.sin(sweep_phase(t, 600, 1400)) * envelope(t, 0.002, 4)
def sound_teleport_warp():
t = time_axis(0.38)
rise = np.sin(sweep_phase(t, 300, 1500)) * (1 + 0.5 * np.sin(2 * math.pi * 24 * t))
return 0.35 * rise * np.sin(math.pi * t / t[-1]) ** 0.5
def sound_teleport():
t = time_axis(0.3)
pop = thump(t, 500, 90, 4) + 0.7 * burst(t, 91, 0.25, 6)
sparkle = 0.4 * ring(t, [2200, 3300], 7)
return 0.8 * np.tanh(pop + sparkle)
HIT_SOUNDS = {name.removeprefix("hit_"): globals()[name] for name in
["hit_blunt", "hit_slash", "hit_pierce", "hit_lash", "hit_bullet", "hit_energy", "hit_fire", "hit_explosive",
"hit_magic"]}
def hits_save():
for name, sound in HIT_SOUNDS.items():
ogg_save(f"hit_{name}", sound(0))
ogg_save(f"hit_{name}_heavy", sound(1))
def sound_select():
t = time_axis(0.04)
return 0.25 * square(2 * math.pi * 1000 * t) * envelope(t, 0.001, 2)
def ogg_save(name, signal):
signal = np.clip(signal, -1, 1)
pcm = (signal * 32767).astype(np.int16)
with tempfile.TemporaryDirectory() as directory:
wavPath = pathlib.Path(directory) / "sound.wav"
with wave.open(str(wavPath), "wb") as file:
file.setnchannels(1)
file.setsampwidth(2)
file.setframerate(RATE)
file.writeframes(pcm.tobytes())
subprocess.run(
["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", str(wavPath), "-c:a", "libvorbis", "-q:a", "4", str(SFX / f"{name}.ogg")],
check=True,
)
def sfx_generate():
SFX.mkdir(parents=True, exist_ok=True)
ogg_save("jump", sound_jump())
ogg_save("land", sound_land())
ogg_save("step", sound_step())
ogg_save("swing", sound_whoosh(0.25, 0.02, 0.3, 3))
ogg_save("slash", sound_whoosh(0.15, 0.1, 0.6, 4))
ogg_save("poke", sound_poke())
ogg_save("blast", sound_blast())
ogg_save("beam", sound_beam())
ogg_save("boomerang", sound_boomerang())
hits_save()
ogg_save("slam", sound_slam())
ogg_save("charge", sound_charge())
ogg_save("ready", sound_ready())
ogg_save("select", sound_select())
ogg_save("bow", sound_twang())
ogg_save("rocket", sound_rocket())
ogg_save("grapple", sound_grapple())
ogg_save("latch", sound_latch())
ogg_save("explosion", sound_explosion())
ogg_save("flail", sound_flail())
ogg_save("flail_land", sound_flail_land())
ogg_save("dash", sound_dash())
ogg_save("climb", sound_climb())
ogg_save("echo", sound_echo())
ogg_save("equip", sound_equip())
ogg_save("unequip", sound_unequip())
ogg_save("swap", sound_swap())
ogg_save("ricochet", sound_ricochet())
ogg_save("pound", sound_pound())
ogg_save("zap", sound_zap())
ogg_save("uzi", sound_uzi())
ogg_save("mine", sound_mine())
ogg_save("death", sound_death())
ogg_save("bounce", sound_bounce())
ogg_save("shotgun", sound_shotgun())
ogg_save("whip", sound_whip())
ogg_save("whip_blast", sound_whip_blast())
ogg_save("flame", sound_flame())
ogg_save("fireball", sound_fireball())
ogg_save("detonate", sound_detonate())
ogg_save("detonate_empty", sound_detonate_empty())
ogg_save("slash_heavy", sound_slash_heavy())
ogg_save("swing_heavy", sound_swing_heavy())
ogg_save("flail_heavy", sound_flail_heavy())
ogg_save("poke_heavy", sound_poke_heavy())
ogg_save("whip_heavy", sound_whip_heavy())
ogg_save("grapple_heavy", sound_grapple_heavy())
ogg_save("boomerang_heavy", sound_boomerang_heavy())
ogg_save("bow_heavy", sound_bow_heavy())
ogg_save("uzi_heavy", sound_uzi_heavy())
ogg_save("shotgun_heavy", sound_shotgun_heavy())
ogg_save("rocket_heavy", sound_rocket_heavy())
ogg_save("break", sound_break())
ogg_save("duck", sound_duck())
ogg_save("unduck", sound_unduck())
ogg_save("crawl", sound_crawl())
ogg_save("roll", sound_roll())
ogg_save("fall", sound_fall())
ogg_save("finish", sound_finish())
ogg_save("impact_blaster", sound_impact_blaster())
ogg_save("impact_bullet", sound_impact_bullet())
ogg_save("impact_arrow", sound_impact_arrow())
ogg_save("impact_boomerang", sound_impact_boomerang())
ogg_save("impact_mine", sound_impact_mine())
ogg_save("punch", sound_punch())
ogg_save("uppercut", sound_uppercut())
ogg_save("cast", sound_cast())
ogg_save("wisp", sound_wisp())
ogg_save("teleport_warp", sound_teleport_warp())
ogg_save("teleport", sound_teleport())
ogg_save("level_up", sound_arpeggio([523, 659, 784, 1047]))
ogg_save("level_down", sound_arpeggio([784, 622, 523, 392]))
if __name__ == "__main__":
sfx_generate()