commit
2007fb7edc
200 files changed
+27710
No files matched your search
@@ -0,0 +1,214 @@
|
||||
# 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 = {}
|
||||
Reference in new issue
Block a user