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>
52 lines
1.5 KiB
GDScript
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)
|