Files
Papay-Shooter/debug/acoustics_test.gd
T
2026-08-02 02:20:02 -04:00

128 lines
4.7 KiB
GDScript

extends SceneTree
## Headless test for the Acoustics system:
## godot --headless --path . -s res://debug/acoustics_test.gd
## Builds walls of different materials between a listener and a source and
## checks occlusion/classification/reflection math behaves as designed.
var _fails := 0
var _frames := 0
var _level: Node3D
func _check(cond: bool, msg: String) -> void:
if cond:
print(" OK: ", msg)
else:
_fails += 1
print(" FAIL: ", msg)
func _wall(pos: Vector3, size: Vector3, acoustic: String) -> StaticBody3D:
var body := StaticBody3D.new()
body.name = "Wall_" + acoustic
body.set_meta("acoustic_material", acoustic)
body.collision_layer = 1
var shape := CollisionShape3D.new()
shape.shape = BoxShape3D.new()
shape.shape.size = size
body.add_child(shape)
_level.add_child(body)
body.position = pos
return body
func _initialize() -> void:
_level = Node3D.new()
root.add_child(_level)
func _process(_delta: float) -> bool:
_frames += 1
if _frames < 5:
return false # let physics register the bodies
if _frames == 5:
print("=== Acoustics tests ===")
var world := _level.get_world_3d()
var listener := Vector3(0, 1, 0)
var source := Vector3(0, 1, -10)
# No wall: clear line
var occ := Acoustics.occlusion(world, listener, source)
_check(occ.surfaces == 0 and occ.db == 0.0, "clear line of sight -> no occlusion")
# Wooden wall between
var w := _wall(Vector3(0, 1, -5), Vector3(6, 4, 0.4), "wood")
return false
elif _frames == 10:
var world := _level.get_world_3d()
var occ := Acoustics.occlusion(world, Vector3(0, 1, 0), Vector3(0, 1, -10))
_check(occ.surfaces == 1, "wooden wall -> 1 occluding surface")
_check(absf(occ.db - Acoustics.MATERIALS.wood.trans_db) < 0.01,
"wood transmission loss applied (%.1f dB)" % occ.db)
_check(occ.cutoff == Acoustics.MATERIALS.wood.trans_cutoff,
"wood lowpass cutoff (%.0f Hz)" % occ.cutoff)
# Swap to concrete: heavier loss, deeper muffle
for c in _level.get_children():
c.set_meta("acoustic_material", "concrete")
var occ2 := Acoustics.occlusion(world, Vector3(0, 1, 0), Vector3(0, 1, -10))
_check(occ2.db < occ.db, "concrete muffles more than wood (%.1f < %.1f dB)" % [occ2.db, occ.db])
_check(occ2.cutoff < occ.cutoff, "concrete cuts more highs (%.0f < %.0f Hz)" % [occ2.cutoff, occ.cutoff])
# Metal: between wood and concrete in loss, more highs than concrete
for c in _level.get_children():
c.set_meta("acoustic_material", "metal")
var occ3 := Acoustics.occlusion(world, Vector3(0, 1, 0), Vector3(0, 1, -10))
_check(occ3.cutoff > occ2.cutoff, "metal passes more highs than concrete")
# Second wall stacks
_wall(Vector3(0, 1, -7), Vector3(6, 4, 0.4), "wood")
return false
elif _frames == 15:
var world := _level.get_world_3d()
var occ := Acoustics.occlusion(world, Vector3(0, 1, 0), Vector3(0, 1, -10))
_check(occ.surfaces == 2, "two walls -> 2 occluding surfaces (got %d)" % occ.surfaces)
# Reflections: a big wall beside the source, far enough that the
# bounce path is >10m longer than the direct path (>30ms later —
# closer echoes perceptually merge and are correctly skipped).
_wall(Vector3(15, 1, -10), Vector3(0.4, 8, 10), "metal")
return false
elif _frames == 20:
var world := _level.get_world_3d()
var refls := Acoustics.reflections(world, Vector3(0, 1, -10), Vector3(0, 1, -2))
_check(refls.size() >= 1, "side wall produces a reflection (got %d)" % refls.size())
if refls.size() >= 1:
_check(refls[0].delay > 0.0 and refls[0].delay <= 0.35,
"reflection delay in range (%.0f ms)" % (refls[0].delay * 1000.0))
_check(refls[0].cutoff == Acoustics.MATERIALS.metal.refl_cutoff,
"reflection carries metal filter")
# Classification fallbacks
var crate := StaticBody3D.new()
crate.name = "wooden_crate_3"
_check(Acoustics.classify(crate) == "wood", "name-based classify: crate -> wood")
crate.free()
# An UNKNOWN acoustic_material must degrade to concrete, not throw.
# Sakura Crossing tagged its ballast "gravel" before gravel existed in
# MATERIALS, and every hitscan impact on it took down occlusion() from
# inside the audio path — a level builder's typo must never be able to
# stop the game making sound.
var odd := StaticBody3D.new()
odd.name = "OddSurface"
odd.set_meta("acoustic_material", "unobtainium")
_check(Acoustics.classify(odd) == "concrete",
"unknown acoustic_material falls back to concrete")
odd.free()
# And every material the level builders actually use must resolve.
for m in ["concrete", "wood", "metal", "glass", "brick", "gravel", "grass"]:
_check(Acoustics.MATERIALS.has(m), "MATERIALS defines '%s'" % m)
print("=== Acoustics results: %s ===" % ("ALL PASSED" if _fails == 0 else "%d FAILED" % _fails))
return true
return false