Procedural sound kit: the whole game has audio, no recordings

Every WAV is synthesized from maths by tools/gen_sounds.py (run through Blender
for its bundled numpy) -- sines, noise and envelopes, seeded and reproducible,
licence-free by construction. Loops (engine, skid, boost, siren) are built from
integer-Hz components over exactly 1 s so the seam is mathematically silent;
noise beds get a crossfaded seam. Seam continuity is verified numerically.

Wiring: pitch-tracked engine loop on every car (idle burble to top-end scream,
bump while boosting), skid loop off slip, player boost whoosh, small/big crash
by jolt with glass on wrecks, two-tone siren on pursuit cops (derby brawlers
stay silent), Crashbreaker boom, gearbox clunk/bog, win/lose stings on
takedowns, outruns, escapes, busts and derby eliminations.

Loop flags live in code (sfx.gd), not per-file .import settings, so a
regenerated wav never silently loses its loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-29 13:05:48 +10:00
parent aac4384c05
commit 589e99c727
31 changed files with 575 additions and 2 deletions

View File

@ -261,8 +261,33 @@ Visual check (opens a window, saves a PNG):
/Applications/Godot.app/Contents/MacOS/Godot --path . --script tests/screenshot.gd -- car=datto_120y mode=crash frames=280 out=shot.png
```
## Sound
Every WAV in `assets/sounds/` is synthesized from maths by
[tools/gen_sounds.py](tools/gen_sounds.py) — sines, noise and envelopes, no
recordings, so the whole kit is licence-free by construction and reproducible
from its seed like everything else in this repo. (Run it through Blender: it's
only there for the bundled numpy, no bpy involved.)
Loop discipline: anything the game loops (engine, skid, boost, siren) is built
from integer-Hz components over exactly 1 s so the seam is mathematically
silent; noise beds get a crossfaded seam. Loop flags are set in code
([sfx.gd](src/sfx.gd)), not per-file import settings, so a regenerated wav never
silently loses its loop.
Wiring: every car runs a pitch-tracked engine loop (the arcade law: idle burble
at rest, screaming near top speed, a bump while boosting) plus a skid loop off
the slip value; the player adds a boost whoosh. Crashes pick small/big by jolt
strength with glass on a wreck, pursuit cops wail a two-tone siren (derby
brawlers don't), the Crashbreaker booms, the gearbox clunks or bogs, and event
results sting up or down.
If the kit ever wants sweetening beyond synthesis, use **CC0 sources only**
(kenney.nl, OpenGameArt filtered to CC0) — CC-BY means shipping attribution
forever and NC-licensed audio doesn't belong in this repo at all.
## Design doc
`docs/BurnoutInfinityShitbox.pdf` — research report on the original game. Still to come:
crash deformation (control-hull vertex displacement, equations in the PDF), oncoming-lane
boost, Road Rage mode, sound, real level GLBs from Blender.
oncoming-lane boost, Road Rage mode, a DEM elevation pass (so the Story Bridge deck
actually rises), and hand-picked crossings for the two scenery-crash junctions.

BIN
assets/sounds/bog.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://qbn0cilhfqwj"
path="res://.godot/imported/bog.wav-8f1da2786c55aeefbeb106dc868c91db.sample"
[deps]
source_file="res://assets/sounds/bog.wav"
dest_files=["res://.godot/imported/bog.wav-8f1da2786c55aeefbeb106dc868c91db.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/boom.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dnwpk58ldx1ky"
path="res://.godot/imported/boom.wav-ce80ab5026d9de2164088103d22abe14.sample"
[deps]
source_file="res://assets/sounds/boom.wav"
dest_files=["res://.godot/imported/boom.wav-ce80ab5026d9de2164088103d22abe14.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/boost.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://da0fblx1gdv5u"
path="res://.godot/imported/boost.wav-810a94b056a1e46df7bb29aadebe3719.sample"
[deps]
source_file="res://assets/sounds/boost.wav"
dest_files=["res://.godot/imported/boost.wav-810a94b056a1e46df7bb29aadebe3719.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/clunk.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bypon8wahkl6m"
path="res://.godot/imported/clunk.wav-39e9719aa8e918e8eeb10576b0be4c5d.sample"
[deps]
source_file="res://assets/sounds/clunk.wav"
dest_files=["res://.godot/imported/clunk.wav-39e9719aa8e918e8eeb10576b0be4c5d.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/crash_big.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bcxbcnytwk7pj"
path="res://.godot/imported/crash_big.wav-4b192579a3fd4414c388af2e6de59ed6.sample"
[deps]
source_file="res://assets/sounds/crash_big.wav"
dest_files=["res://.godot/imported/crash_big.wav-4b192579a3fd4414c388af2e6de59ed6.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://ceeyt53afugxp"
path="res://.godot/imported/crash_small.wav-94e44666608124db8d2651ef7b97a9aa.sample"
[deps]
source_file="res://assets/sounds/crash_small.wav"
dest_files=["res://.godot/imported/crash_small.wav-94e44666608124db8d2651ef7b97a9aa.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/engine.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cybkeltoy7j3a"
path="res://.godot/imported/engine.wav-d6786269ebfd2ada75bdd079c2e17f80.sample"
[deps]
source_file="res://assets/sounds/engine.wav"
dest_files=["res://.godot/imported/engine.wav-d6786269ebfd2ada75bdd079c2e17f80.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/glass.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cttvhh5nm5j1m"
path="res://.godot/imported/glass.wav-8cd29b0619eb51b1e55ea8a7e038a004.sample"
[deps]
source_file="res://assets/sounds/glass.wav"
dest_files=["res://.godot/imported/glass.wav-8cd29b0619eb51b1e55ea8a7e038a004.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/siren.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cwrt3oul3vs3m"
path="res://.godot/imported/siren.wav-847d6fbddcbf15b051f8f55cc9265888.sample"
[deps]
source_file="res://assets/sounds/siren.wav"
dest_files=["res://.godot/imported/siren.wav-847d6fbddcbf15b051f8f55cc9265888.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/skid.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://xhfwat50fi7v"
path="res://.godot/imported/skid.wav-2935abfa7678803791f7dd569a3dc250.sample"
[deps]
source_file="res://assets/sounds/skid.wav"
dest_files=["res://.godot/imported/skid.wav-2935abfa7678803791f7dd569a3dc250.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dw8p7p4q4y413"
path="res://.godot/imported/sting_lose.wav-6123e9e6785ae293e80b5df7e1e06e37.sample"
[deps]
source_file="res://assets/sounds/sting_lose.wav"
dest_files=["res://.godot/imported/sting_lose.wav-6123e9e6785ae293e80b5df7e1e06e37.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

BIN
assets/sounds/sting_win.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dskukm4ylh7n7"
path="res://.godot/imported/sting_win.wav-99ef8cb7f30234f01743fb8f02166015.sample"
[deps]
source_file="res://assets/sounds/sting_win.wav"
dest_files=["res://.godot/imported/sting_win.wav-99ef8cb7f30234f01743fb8f02166015.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

View File

@ -43,6 +43,9 @@ var _prev_lv := Vector3.ZERO
var _impact_cd := 0.0
var _parts: Array[Node] = []
var _deform: Deformer = null
var _snd_eng: AudioStreamPlayer3D
var _snd_skid: AudioStreamPlayer3D
var _snd_boost: AudioStreamPlayer3D
@onready var wheels: Array[Node] = [$FL, $FR, $RL, $RR]
@ -71,6 +74,13 @@ func _ready() -> void:
var bm := BoxMesh.new()
bm.size = Vector3(stats.width, 1.0, stats.length)
$Body.mesh = bm
# the player's own engine sits front and centre; everyone else's is scenery
_snd_eng = Sfx.looper(self, "engine", -9.0 if is_player else -17.0)
if _snd_eng:
_snd_eng.play()
_snd_skid = Sfx.looper(self, "skid", -7.0 if is_player else -14.0)
if is_player:
_snd_boost = Sfx.looper(self, "boost", -8.0)
func _physics_process(delta: float) -> void:
if is_player:
@ -123,6 +133,25 @@ func _physics_process(delta: float) -> void:
if boosting:
boost = maxf(boost - BOOST_DRAIN * delta, 0.0)
if _snd_eng:
# arcade pitch law: idle burble at rest, screaming near top speed
_snd_eng.pitch_scale = clampf(0.55 + linear_velocity.length() / 26.0, 0.55, 2.5) \
+ (0.25 if boosting else 0.0)
_snd_eng.volume_db = (-9.0 if is_player else -17.0) + absf(throttle_in) * 4.0
if _snd_skid:
var skidding := grounded >= 2 and absf(slip) > 4.0 and linear_velocity.length() > 8.0
if skidding and not _snd_skid.playing:
_snd_skid.play()
elif not skidding and _snd_skid.playing:
_snd_skid.stop()
if skidding:
_snd_skid.pitch_scale = clampf(0.85 + absf(slip) / 40.0, 0.85, 1.4)
if _snd_boost:
if boosting and not _snd_boost.playing:
_snd_boost.play()
elif not boosting and _snd_boost.playing:
_snd_boost.stop()
var jolt := linear_velocity - _prev_lv
_prev_lv = linear_velocity
_impact_cd = maxf(_impact_cd - delta, 0.0)
@ -136,6 +165,10 @@ func earn_boost(amount: float) -> void:
func _impact(jolt: Vector3) -> void:
var dv := jolt.length()
crashed.emit(dv)
Sfx.shot(self, "crash_big" if dv > 8.0 else "crash_small",
-2.0, randf_range(0.9, 1.1))
if dv > WRECK_DV:
Sfx.shot(self, "glass", -4.0)
var side := (global_basis.inverse() * -jolt).normalized()
if _deform:
# no contact manifold from the jolt heuristic, so approximate the strike

View File

@ -9,9 +9,18 @@ extends Car
var target: Node3D
var power_boost := 1.0 # scales with heat: more wanted = more committed
var siren := true # pursuit cops wail; derby brawlers don't
var _stuck := 0.0
var _unstick := 0.0
var _snd_siren: AudioStreamPlayer3D
func _ready() -> void:
super()
if siren:
_snd_siren = Sfx.looper(self, "siren", -8.0)
if _snd_siren:
_snd_siren.play()
func _physics_process(delta: float) -> void:
if target and not wrecked:

View File

@ -203,6 +203,7 @@ func _ready() -> void:
var d: Cop = (load("res://src/cop.tscn") as PackedScene).instantiate()
d.stats = load(pool[randi() % pool.size()])
d.power_boost = 1.0
d.siren = false # brawlers, not police
# a wreck is an elimination: without this car.gd's 2.2 s _recover
# timer stands them back up and the field never empties
d.hold_wreck = true
@ -594,6 +595,7 @@ func _derby_down(d: Cop) -> void:
derby_kills += 1
car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP)
car.boost = car.boost_max
Sfx.shot(car, "sting_win", -4.0)
_msg("WRECKED 'EM! x%d" % derby_kills, 46)
func _derby_tick(delta: float) -> void:
@ -642,10 +644,12 @@ func _drag_tick(delta: float) -> void:
if Input.is_action_just_pressed("boost"):
if revs >= SHIFT_LO and revs < REDLINE:
clean_shifts += 1
Sfx.shot(car, "clunk", -2.0)
_msg("CLEAN SHIFT", 34)
else:
blown_shifts += 1
bog = 0.9
Sfx.shot(car, "bog", -2.0)
_msg("MISSED IT", 34)
gear += 1
revs = 0.28 if gear <= DRAG_GEARS else 1.0
@ -727,9 +731,11 @@ func _end_pursuit(escaped: bool) -> void:
escapes += 1
car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP)
car.boost = car.boost_max
Sfx.shot(car, "sting_win", -4.0)
_msg("LOST 'EM! x%d" % escapes, 52)
else:
busts += 1
Sfx.shot(car, "sting_lose", -4.0)
_msg("BUSTED", 52)
func _pursuit_tick(delta: float) -> void:
@ -831,8 +837,10 @@ func _outrun_end(won: bool, why: String) -> void:
outruns_won += 1
car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) # takedown-tier reward
car.boost = car.boost_max
Sfx.shot(car, "sting_win", -4.0)
_msg("OUTRUN WON! x%d%s" % [outruns_won, tail], 52)
else:
Sfx.shot(car, "sting_lose", -4.0)
_msg("OUTRUN LOST%s" % tail, 44)
func _race_tick() -> void:
@ -894,6 +902,7 @@ func _rival_wrecked(r: Rival) -> void:
return
car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP)
car.boost = car.boost_max
Sfx.shot(car, "sting_win", -4.0)
_msg("TAKEDOWN!", 56)
func _traffic_smashed(impulse: float) -> void:
@ -927,6 +936,7 @@ func _crashbreaker() -> void:
else:
(b as RigidBody3D).apply_central_impulse(dir * strength * (b as RigidBody3D).mass)
car.apply_central_impulse(Vector3.UP * car.mass * 7.0)
Sfx.shot(car, "boom", 0.0)
_msg("CRASHBREAKER!", 52)
var boom := MeshInstance3D.new()
var sm := SphereMesh.new()

51
src/sfx.gd Normal file
View File

@ -0,0 +1,51 @@
class_name Sfx
## Tiny audio utility over the procedural kit in assets/sounds (tools/gen_sounds.py).
## Loops are flagged here rather than in per-file .import settings so a wav
## regenerated by the tool never silently loses its loop flag.
const DIR := "res://assets/sounds/%s.wav"
const LOOPS := ["engine", "skid", "boost", "siren"]
static var _cache := {}
static func stream(name: String) -> AudioStream:
if _cache.has(name):
return _cache[name]
var path := DIR % name
if not ResourceLoader.exists(path):
return null
var s: AudioStream = load(path)
if s is AudioStreamWAV and name in LOOPS:
var w := s as AudioStreamWAV
w.loop_mode = AudioStreamWAV.LOOP_FORWARD
w.loop_begin = 0
w.loop_end = w.data.size() / 2 # 16-bit mono: 2 bytes per frame
_cache[name] = s
return s
static func looper(parent: Node, name: String, db: float) -> AudioStreamPlayer3D:
## Persistent 3D player for a looped bed (engine, skid, siren). Not playing
## until the caller decides; returns null when the kit isn't generated yet.
var s := stream(name)
if s == null:
return null
var p := AudioStreamPlayer3D.new()
p.stream = s
p.volume_db = db
p.max_distance = 90.0
parent.add_child(p)
return p
static func shot(parent: Node, name: String, db := 0.0, pitch := 1.0) -> void:
## Fire-and-forget one-shot at the parent's position.
var s := stream(name)
if s == null:
return
var p := AudioStreamPlayer3D.new()
p.stream = s
p.volume_db = db
p.pitch_scale = pitch
p.max_distance = 160.0
parent.add_child(p)
p.play()
p.finished.connect(p.queue_free)

1
src/sfx.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://bf8uwanb2pyhb

156
tools/gen_sounds.py Normal file
View File

@ -0,0 +1,156 @@
"""Procedural sound kit: every WAV the game plays, synthesized from maths.
Run: /Applications/Blender.app/Contents/MacOS/Blender -b -P tools/gen_sounds.py
(Blender only for its bundled numpy -- no bpy used)
Licence-free by construction: sines, noise and envelopes, no recordings.
Deterministic (seeded), so a rebuild is byte-for-byte reproducible.
Loop discipline: anything the game loops (engine, skid, boost, siren) is built
from integer-Hz components over an exact 1 s so the seam is mathematically
silent; noise beds get a crossfaded seam instead. One-shots just decay to zero.
"""
import math
import os
import wave
import numpy as np
SR = 22050
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets", "sounds")
rng = np.random.default_rng(1982)
def t(dur):
return np.arange(int(SR * dur)) / SR
def env_exp(dur, tau):
return np.exp(-t(dur) / tau)
def noise(dur):
return rng.standard_normal(int(SR * dur))
def band(x, lo, hi, peaks=()):
"""FFT bandpass with optional resonant peaks [(hz, width, gain)]."""
spec = np.fft.rfft(x)
f = np.fft.rfftfreq(len(x), 1 / SR)
mask = ((f >= lo) & (f <= hi)).astype(float)
edge = (hi - lo) * 0.15 + 1.0
mask = np.convolve(mask, np.hanning(9) / np.hanning(9).sum(), mode="same")
for hz, width, gain in peaks:
mask += gain * np.exp(-((f - hz) / width) ** 2)
return np.fft.irfft(spec * mask, len(x))
def seam(x, fade=0.06):
"""Crossfade the tail into the head so a noise bed loops cleanly."""
n = int(SR * fade)
w = np.linspace(0, 1, n)
x[:n] = x[:n] * w + x[-n:] * (1 - w)
return x[:-n]
def norm(x, peak=0.85):
return x / (np.max(np.abs(x)) + 1e-9) * peak
def write(name, x):
x16 = (np.clip(x, -1, 1) * 32767).astype("<i2")
with wave.open(os.path.join(OUT, name + ".wav"), "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(SR)
w.writeframes(x16.tobytes())
print("%-14s %5.2fs peak %.2f" % (name, len(x) / SR, np.max(np.abs(x))))
os.makedirs(OUT, exist_ok=True)
# engine: integer-Hz harmonic stack of 56 Hz with a firing-pulse AM -- the
# arcade "brap". All components integer Hz over exactly 1 s = seamless loop.
tt = t(1.0)
eng = np.zeros(len(tt))
for h, a in ((1, 1.0), (2, 0.62), (3, 0.42), (4, 0.30), (6, 0.16), (8, 0.09)):
eng += a * np.sin(2 * math.pi * 56 * h * tt + h)
eng += 0.5 * np.sin(2 * math.pi * 28 * tt) # sub
eng *= 1.0 + 0.38 * np.sin(2 * math.pi * 28 * tt - 0.7) # firing pulses
bed = band(noise(1.0), 40, 900)
eng[: len(seam(bed))] += 0.10 * norm(seam(bed), 1.0)[: len(eng)]
write("engine", norm(eng, 0.7))
# skid: screechy banded noise with resonances and a 15 Hz tremolo
sk = band(noise(1.0), 900, 3200, peaks=((1250, 90, 6.0), (1900, 120, 4.0)))
sk *= 1.0 + 0.3 * np.sin(2 * math.pi * 15 * t(1.0))
write("skid", norm(seam(sk), 0.6))
# boost: airy rushing band with a slow 6 Hz surge
bo = band(noise(1.0), 250, 1600) + 0.5 * band(noise(1.0), 2500, 6000)
bo *= 1.0 + 0.25 * np.sin(2 * math.pi * 6 * t(1.0))
write("boost", norm(seam(bo), 0.55))
# siren: Aussie two-tone, 620/470 Hz halves with soft crossfades, odd harmonics
tt = t(1.0)
gate = 0.5 * (1 + np.tanh(np.sin(2 * math.pi * 1 * tt) * 24)) # smooth 0/1 square
si = np.zeros(len(tt))
for f0, g in ((620, gate), (470, 1 - gate)):
tone = sum(np.sin(2 * math.pi * f0 * k * tt) / k for k in (1, 3, 5))
si += g * tone
write("siren", norm(si, 0.5))
# crashes: thump sweep + banded burst (+ clatter echoes on the big one)
def thump(dur, f_hi, f_lo, tau):
ph = 2 * math.pi * np.cumsum(np.linspace(f_hi, f_lo, int(SR * dur))) / SR
return np.sin(ph) * env_exp(dur, tau)
cs = thump(0.35, 150, 48, 0.09)
cs += 0.8 * band(noise(0.35), 200, 4200) * env_exp(0.35, 0.05)
cs += 0.25 * (np.sin(2 * math.pi * 810 * t(0.35)) + np.sin(2 * math.pi * 1130 * t(0.35))) * env_exp(0.35, 0.03)
write("crash_small", norm(cs, 0.8))
cb = thump(0.9, 120, 34, 0.22)
cb += 0.9 * band(noise(0.9), 120, 3800) * env_exp(0.9, 0.13)
for d, a in ((0.16, 0.4), (0.29, 0.3), (0.45, 0.2)): # clatter
i = int(SR * d)
burst = band(noise(0.12), 400, 3000) * env_exp(0.12, 0.03) * a
cb[i:i + len(burst)] += burst
write("crash_big", norm(cb, 0.9))
# glass: a shower of tiny bright pings over a sizzle
gl = 0.25 * band(noise(0.6), 4000, 9500) * env_exp(0.6, 0.12)
for _ in range(42):
at = rng.uniform(0, 0.4)
f = rng.uniform(3800, 8800)
ping = np.sin(2 * math.pi * f * t(0.05)) * env_exp(0.05, 0.012) * rng.uniform(0.2, 0.55)
i = int(SR * at)
gl[i:i + len(ping)] += ping
write("glass", norm(gl, 0.6))
# crashbreaker: deep sweep + saturated noise whomp
bm = thump(1.2, 95, 28, 0.35)
bm += 1.2 * band(noise(1.2), 30, 700) * env_exp(1.2, 0.18)
write("boom", norm(np.tanh(bm * 2.2), 0.95))
# stings: little square arpeggios (win up, lose down)
def sting(notes, dur=0.16, gap=0.02):
parts = []
for f in notes:
tone = sum(np.sin(2 * math.pi * f * k * t(dur)) / k for k in (1, 3)) * env_exp(dur, 0.09)
parts += [tone, np.zeros(int(SR * gap))]
return np.concatenate(parts)
write("sting_win", norm(sting([392, 523, 659, 784]), 0.55))
write("sting_lose", norm(sting([392, 311, 233], dur=0.22), 0.5))
# gearbox: clean clunk vs blown-shift bog
ck = thump(0.14, 95, 60, 0.03) + 0.5 * band(noise(0.14), 1500, 5000) * env_exp(0.14, 0.006)
write("clunk", norm(ck, 0.7))
ph = 2 * math.pi * np.cumsum(np.linspace(130, 55, int(SR * 0.45))) / SR
bg = np.sign(np.sin(ph)) * env_exp(0.45, 0.16)
bg *= 1.0 + 0.5 * np.sin(2 * math.pi * 27 * t(0.45))
write("bog", norm(bg * 0.5 + 0.3 * band(noise(0.45), 80, 500) * env_exp(0.45, 0.1), 0.6))
print("sound kit written to", os.path.abspath(OUT))