# 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()