# Candidates for the sounds the review is still choosing, and the chip-style synth recipes some of them use. # Each candidate is a pick in tools/sfx_build.py's form (a layer, or a list of layers), so it's auditioned crunched # exactly as the game's sound would be, and choosing it in the review means copying it into PICKS. A layer is either # cut from a library track ({"f": ..., "s": ...}) or synthesized here ({"synth": recipe, "a": {arguments}}). import math import numpy as np RATE = 44100 def time_axis(seconds): return np.arange(int(RATE * seconds)) / RATE def envelope(t, attack, power): rise = np.clip(t / max(attack, 1e-4), 0, 1) return rise * (1 - t / max(t[-1], 1e-9)) ** power def glide(t, start, end): # Exponential, so it sweeps evenly by ear. return start * (end / start) ** (t / max(t[-1], 1e-9)) def phase(frequency): return 2 * math.pi * np.cumsum(frequency) / RATE def pulse(angle, duty=0.5): return np.where((angle / (2 * math.pi)) % 1 < duty, 1.0, -1.0) def noise(count, seed): return np.random.default_rng(seed).uniform(-1, 1, count) def lowpass(signal, amount): amounts = np.broadcast_to(amount, signal.shape) out = np.zeros_like(signal) value = 0.0 for i, sample in enumerate(signal): value += (sample - value) * amounts[i] out[i] = value return out def at(signal, part, seconds): start = int(seconds * RATE) end = min(len(signal), start + len(part)) signal[start:end] += part[:end - start] return signal # A pulse-wave laser: a fast falling sweep, optionally with an octave above and a wobble. def pew(start=1600, end=200, seconds=0.14, duty=0.5, octave=0.0, vibrato=0.0): t = time_axis(seconds) frequency = glide(t, start, end) * (1 + 0.08 * np.sin(2 * math.pi * vibrato * t)) tone = pulse(phase(frequency), duty) + octave * pulse(phase(frequency * 2), duty) return 0.5 * tone * envelope(t, 0.002, 1.3) # Notes one after another (Hz), on a pulse wave. def arp(notes=(523, 659, 784, 1047), seconds=0.3, duty=0.5): t = time_axis(seconds) step = len(t) / len(notes) frequency = np.array(notes, dtype=float)[np.minimum((np.arange(len(t)) / step).astype(int), len(notes) - 1)] return 0.45 * pulse(phase(frequency), duty) * envelope(t, 0.002, 0.8) def slide(start=400, end=1200, seconds=0.3, duty=0.5, vibrato=0.0): t = time_axis(seconds) frequency = glide(t, start, end) * (1 + 0.06 * np.sin(2 * math.pi * vibrato * t)) return 0.45 * pulse(phase(frequency), duty) * envelope(t, 0.004, 1) def beeps(frequency=1800, count=2, length=0.045, gap=0.07, duty=0.5): t = time_axis(length) beep = 0.5 * pulse(2 * math.pi * frequency * t, duty) * envelope(t, 0.001, 0.4) signal = np.zeros(int(RATE * (gap * (count - 1) + length)) + 1) for i in range(count): at(signal, beep, i * gap) return signal # Short blips repeating and fading, like an echo. def echo_blips(frequency=1400, repeats=3, gap=0.07, duty=0.25): t = time_axis(0.05) blip = pulse(phase(glide(t, frequency, frequency * 0.7)), duty) * envelope(t, 0.001, 1.5) signal = np.zeros(int(RATE * (gap * repeats + 0.05)) + 1) for i in range(repeats): at(signal, 0.5 * blip * 0.55 ** i, i * gap) return signal def shimmer(frequency=1400, rate=22, seconds=0.28, duty=0.5): t = time_axis(seconds) tone = pulse(2 * math.pi * frequency * t, duty) * (0.6 + 0.4 * np.sin(2 * math.pi * rate * t)) return 0.4 * tone * envelope(t, 0.005, 2) # A ring-modulated chirp: metallic, bell-ish and odd. def ring_chirp(start=1800, end=900, seconds=0.22, ring=330): t = time_axis(seconds) return 0.6 * np.sin(phase(glide(t, start, end))) * np.sin(2 * math.pi * ring * t) * envelope(t, 0.002, 1.5) # Rising, wobbling tone: a warp gathering or a charge building. def warp(start=300, end=1500, seconds=0.38, wobble=24, duty=0.5, depth=0.5): t = time_axis(seconds) tone = pulse(phase(glide(t, start, end)), duty) * (1 + depth * np.sin(2 * math.pi * wobble * t)) return 0.3 * tone * np.sin(math.pi * t / t[-1]) ** 0.5 def charge(start=180, end=520, seconds=0.8, duty=0.5, shimmer=12, rumble=0.0): t = time_axis(seconds) tone = pulse(phase(glide(t, start, end)), duty) * (0.75 + 0.25 * np.sin(2 * math.pi * shimmer * t)) low = rumble * lowpass(noise(len(t), 3), 0.03) * 4 return 0.3 * (tone + low) * np.clip(t / 0.12, 0, 1) # Noise swept from dark to bright and back, with a rising roar under it: a big whoosh. def whoosh(seconds=0.45, low=0.03, high=0.5, seed=62, roar=0.6, roarStart=90, roarEnd=260, drive=1.8): t = time_axis(seconds) sweep = lowpass(noise(len(t), seed), np.linspace(low, high, len(t))) * np.sin(math.pi * t / t[-1]) ** 0.6 tone = np.sin(phase(glide(t, roarStart, roarEnd))) * envelope(t, 0.01, 2.5) return 0.95 * np.tanh(drive * (1.4 * sweep + roar * tone)) # Embers: a soft hiss swelling up with crackling pops scattered through it. def ember(seconds=0.4, seed=7, density=60, hiss=0.5, swell=0.3): t = time_axis(seconds) rng = np.random.default_rng(seed) body = hiss * lowpass(noise(len(t), seed), 0.12) * np.clip(t / max(swell * seconds, 1e-3), 0, 1) pops = np.zeros(len(t)) for start in rng.uniform(0, seconds * 0.9, int(density * seconds)): size = int(rng.uniform(0.001, 0.004) * RATE) at(pops, rng.uniform(0.3, 1) * noise(size, int(start * 1e6)) * np.linspace(1, 0, size), start) return 0.9 * (body + pops) * envelope(t, 0.02, 0.8) # Sparkles: quick high pings at random pitches over a crackle, with an optional thump for weight. def sparkle(seconds=0.15, seed=11, density=90, low=2000, high=5000, crackle=0.5, thump=0.0): t = time_axis(seconds) rng = np.random.default_rng(seed) signal = np.zeros(len(t)) for start in rng.uniform(0, seconds * 0.8, max(3, int(density * seconds))): ping = time_axis(rng.uniform(0.015, 0.04)) at(signal, 0.5 * np.sin(2 * math.pi * rng.uniform(low, high) * ping) * envelope(ping, 0.0005, 2), start) signal += crackle * np.where(rng.random(len(t)) < 0.02, rng.uniform(-1, 1, len(t)), 0) signal += thump * np.sin(phase(glide(t, 180, 60))) * envelope(t, 0.001, 3) return signal * envelope(t, 0.001, 1.2) # A dull, low thud: a falling sine with a muffled noise body. def thud(start=140, end=45, seconds=0.35, body=0.6, seed=9): t = time_axis(seconds) boom = np.sin(phase(glide(t, start, end))) * envelope(t, 0.002, 2) return 0.8 * np.tanh(1.5 * (boom + body * lowpass(noise(len(t), seed), 0.04) * 4 * envelope(t, 0.002, 3))) # An arrival pop: a falling thump and a burst, with a sparkle over it. def pop(start=500, end=90, seconds=0.3, glitter=0.4): t = time_axis(seconds) body = np.sin(phase(glide(t, start, end))) * envelope(t, 0.001, 4) + 0.7 * lowpass(noise(len(t), 91), 0.25) * envelope(t, 0.0005, 6) shine = glitter * (np.sin(2 * math.pi * 2200 * t) + 0.5 * np.sin(2 * math.pi * 3300 * t)) * envelope(t, 0.001, 7) return 0.8 * np.tanh(body + shine) def bell(frequency=1320, seconds=0.45): t = time_axis(seconds) tone = sum(np.sin(2 * math.pi * frequency * ratio * t) * weight for ratio, weight in [(1, 1), (1.5, 0.6), (2, 0.25), (2.76, 0.15)]) return 0.35 * tone * envelope(t, 0.002, 3) # A plucked string (Karplus-Strong): a burst of noise looping through a delay the length of one period, softened each # pass, so it rings at the string's pitch and dulls as it dies. def pluck(frequency=180, seconds=0.3, damping=0.996): count = int(RATE * seconds) period = max(2, int(RATE / frequency)) buffer = list(noise(period, 21)) out = np.zeros(count) for i in range(count): first = buffer.pop(0) out[i] = first buffer.append(damping * 0.5 * (first + buffer[0])) return 0.9 * out # The first synthesized mine: a clunk with a short beep after it. def mine_clunk(): t = time_axis(0.12) clunk = np.sin(phase(glide(t, 220, 120))) * envelope(t, 0.001, 8) + lowpass(noise(len(t), 32), 0.5) * envelope(t, 0.001, 14) return 0.6 * clunk SYNTHS = {function.__name__: function for function in [pew, arp, slide, beeps, echo_blips, shimmer, ring_chirp, warp, charge, whoosh, ember, sparkle, thud, pop, bell, pluck, mine_clunk]} def L(f, s, n, **options): return {"f": f, "s": s, "n": n, **options} def S(recipe, **arguments): options = {key: arguments.pop(key) for key in ["g", "at", "r"] if key in arguments} return {"synth": recipe, "a": arguments, **options} # For each sound still being chosen: candidates as (label, pick), the first being what the game uses meanwhile. All # chosen for now; a new review round puts the open sounds' candidates here. SUITES = {}