ShitboxInfinity/src/sfx.gd
m3ultra 589e99c727 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>
2026-07-29 13:05:48 +10:00

52 lines
1.5 KiB
GDScript

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)