first commit
This commit is contained in:
245
tests/audio_test.gd
Normal file
245
tests/audio_test.gd
Normal file
@@ -0,0 +1,245 @@
|
||||
extends Node
|
||||
## Prüft Klangerzeugung und Wiedergabe.
|
||||
var main
|
||||
var step := 0
|
||||
var wait := 0.0
|
||||
var ok := 0
|
||||
var fail := 0
|
||||
var worst := 0.0
|
||||
var elapsed := 0.0
|
||||
|
||||
func chk(n: String, c: bool, i := "") -> void:
|
||||
if c: ok += 1; print(" OK %s %s" % [n, i])
|
||||
else: fail += 1; print(" FAIL %s %s" % [n, i])
|
||||
|
||||
func _key(c: int, d: bool) -> void:
|
||||
var e := InputEventKey.new(); e.physical_keycode = c; e.keycode = c; e.pressed = d
|
||||
Input.parse_input_event(e)
|
||||
|
||||
func _process(d):
|
||||
if main == null:
|
||||
main = get_tree().current_scene
|
||||
return
|
||||
if not main.started:
|
||||
main._start_solo()
|
||||
return
|
||||
if not main.audio.loaded:
|
||||
elapsed += d
|
||||
worst = maxf(worst, d)
|
||||
return
|
||||
if step == 0:
|
||||
chk("Kein Ruckler beim Erzeugen", worst < 0.2,
|
||||
"längster Frame %.0f ms in %.1f s Ladezeit" % [worst * 1000.0, elapsed])
|
||||
if wait > 0.0:
|
||||
wait -= d
|
||||
return
|
||||
var a = main.audio
|
||||
match step:
|
||||
0:
|
||||
_check_harmony()
|
||||
_check_shapes()
|
||||
_check_loops()
|
||||
var names := ["clack", "clack2", "rumble", "squeal", "whistle", "hiss",
|
||||
"bell", "coin", "pour", "clink", "thud", "step", "click", "deny"]
|
||||
var missing: Array[String] = []
|
||||
for n in names:
|
||||
if not a.sounds.has(n):
|
||||
missing.append(n)
|
||||
chk("Alle Klänge da", missing.is_empty(), "fehlt: %s" % [missing])
|
||||
var silent: Array[String] = []
|
||||
for n in a.sounds.keys():
|
||||
var w: AudioStreamWAV = a.sounds[n]
|
||||
if w.data.size() < 200 or _peak(w) < 1000:
|
||||
silent.append(String(n))
|
||||
chk("Kein Klang ist stumm", silent.is_empty(), "still: %s" % [silent])
|
||||
chk("Musik erzeugt", a._music.stream != null and a._music.playing)
|
||||
chk("Thread beendet", a._thread == null)
|
||||
var m: AudioStreamWAV = a._music.stream
|
||||
chk("Musik ist eine Schleife", m.loop_mode == AudioStreamWAV.LOOP_FORWARD)
|
||||
chk("Musik lang genug", m.data.size() / 2 > 20 * 22050,
|
||||
"%.1f s" % (m.data.size() / 2.0 / 22050.0))
|
||||
chk("Musik nicht stumm", _peak(m) > 3000, "peak=%d" % _peak(m))
|
||||
chk("Rumble läuft leise", a._rumble.playing and a._rumble.volume_db < -40.0,
|
||||
"db=%.0f" % a._rumble.volume_db)
|
||||
wait = 0.3
|
||||
1:
|
||||
# Losfahren: Rumble muss lauter werden, Radschläge kommen
|
||||
main._srv_toggle_drive(1)
|
||||
a.clacks = 0
|
||||
_key(KEY_W, true)
|
||||
wait = 9.0
|
||||
2:
|
||||
chk("Rumble folgt dem Tempo", a._rumble.volume_db > -30.0,
|
||||
"v=%.1f db=%.0f" % [main.train.speed, a._rumble.volume_db])
|
||||
chk("Radschläge kommen", a.clacks >= 3, "clacks=%d" % a.clacks)
|
||||
_key(KEY_W, false)
|
||||
_key(KEY_S, true)
|
||||
wait = 0.5
|
||||
3:
|
||||
chk("Bremsen quietscht", a._squeal.volume_db > -60.0,
|
||||
"db=%.0f" % a._squeal.volume_db)
|
||||
# Pfeife
|
||||
_key(KEY_H, true); _key(KEY_H, false)
|
||||
wait = 0.2
|
||||
4:
|
||||
var whistling := false
|
||||
for p in a._pool3d:
|
||||
if p.playing and p.stream == a.sounds["whistle"]:
|
||||
whistling = true
|
||||
chk("H pfeift", whistling)
|
||||
wait = 3.0
|
||||
5:
|
||||
_key(KEY_S, false)
|
||||
chk("Zug steht", main.train.speed < 0.2, "v=%.2f" % main.train.speed)
|
||||
chk("Rumble wieder still", a._rumble.volume_db < -40.0,
|
||||
"db=%.0f" % a._rumble.volume_db)
|
||||
_key(KEY_M, true); _key(KEY_M, false)
|
||||
wait = 0.2
|
||||
6:
|
||||
chk("M schaltet Musik aus", not a.music_on and a._music.volume_db < -60.0)
|
||||
_key(KEY_M, true); _key(KEY_M, false)
|
||||
wait = 0.2
|
||||
7:
|
||||
chk("M schaltet Musik wieder an", a.music_on and a._music.volume_db > -30.0)
|
||||
# Ankunftsglocke und Auszischen gezielt auslösen
|
||||
main._last_dock = null
|
||||
main._park_at(main.stations[1])
|
||||
a.last_played = ""
|
||||
main._update_audio(0.016)
|
||||
chk("Glocke bei Ankunft", a.last_played == "bell", "zuletzt=%s" % a.last_played)
|
||||
main._last_speed = 6.0
|
||||
main.train.speed = 0.0
|
||||
a.last_played = ""
|
||||
main._update_audio(0.016)
|
||||
chk("Zischen beim Halten", a.last_played == "hiss", "zuletzt=%s" % a.last_played)
|
||||
wait = 0.2
|
||||
8:
|
||||
print("== %d ok, %d fail ==" % [ok, fail])
|
||||
get_tree().quit(1 if fail > 0 else 0)
|
||||
step += 1
|
||||
|
||||
func _peak(w: AudioStreamWAV) -> int:
|
||||
var d := w.data
|
||||
var n: int = mini(d.size() / 2, 300000)
|
||||
var peak := 0
|
||||
for i in range(0, n, 7):
|
||||
var v: int = absi(d.decode_s16(i * 2))
|
||||
if v > peak:
|
||||
peak = v
|
||||
return peak
|
||||
|
||||
## Kein Ton darf eine kleine Sekunde oder kleine None gegen eine andere
|
||||
## Stimme bilden. Große Septimen sind erlaubt - die gehören zum Klang.
|
||||
## An genau dieser Regel ist das C über G6 aufgefallen.
|
||||
func _check_harmony() -> void:
|
||||
var Sfx = load("res://scripts/sfx.gd")
|
||||
var bad: Array[String] = []
|
||||
for stage in Sfx.PROGRESSION:
|
||||
var voices: Array[float] = []
|
||||
for f in stage["pad"]:
|
||||
voices.append(float(f))
|
||||
voices.append(float(stage["bass"]))
|
||||
# Flächen und Bass untereinander
|
||||
for i in voices.size():
|
||||
for j in range(i + 1, voices.size()):
|
||||
_flag(bad, String(stage["name"]), voices[i], voices[j])
|
||||
# Melodie gegen jede Stimme
|
||||
for f in stage["melody"]:
|
||||
for v in voices:
|
||||
_flag(bad, String(stage["name"]), float(f), v)
|
||||
chk("Keine Halbton- oder None-Reibung", bad.is_empty(), "%s" % [bad])
|
||||
|
||||
func _flag(bad: Array[String], chord: String, a: float, b: float) -> void:
|
||||
var d: int = absi(int(round(12.0 * (log(a / b) / log(2.0)))))
|
||||
if d == 1 or d == 13:
|
||||
bad.append("%s: %s gegen %s (%d HT)" % [chord, _name(_pc(a)), _name(_pc(b)), d])
|
||||
|
||||
func _pc(f: float) -> int:
|
||||
return int(round(69.0 + 12.0 * (log(f / 440.0) / log(2.0)))) % 12
|
||||
|
||||
func _name(pc: int) -> String:
|
||||
return ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][pc]
|
||||
|
||||
## Form der Schlaggeräusche: lang genug und mit Tiefe, nicht nur ein Tick.
|
||||
func _check_shapes() -> void:
|
||||
for n in ["clack", "clack2", "step", "thud"]:
|
||||
var x := _samples(n)
|
||||
chk("%s klingt aus" % n, _tail(x) > 0.18, "%.2f s hörbar" % _tail(x))
|
||||
chk("%s hat Tiefe" % n, _zcr(x, 0, x.size()) < 0.22,
|
||||
"Nulldurchgänge %.3f je Sample" % _zcr(x, 0, x.size()))
|
||||
for n in ["clack", "clack2"]:
|
||||
var r := _double_hit(_samples(n))
|
||||
chk("%s ist ein Doppelschlag" % n, bool(r["ok"]),
|
||||
"zweiter Schlag bei %.3f s, Einbruch davor auf %.0f %%"
|
||||
% [r["at"], float(r["dip"]) * 100.0])
|
||||
|
||||
## Schleifen dürfen an der Naht nicht knicken, und das Quietschen darf
|
||||
## seine Tonhöhe nicht weglaufen lassen.
|
||||
func _check_loops() -> void:
|
||||
for n in ["squeal", "rumble"]:
|
||||
var x := _samples(n)
|
||||
var kink: float = absf(x[0] - (2.0 * x[x.size() - 1] - x[x.size() - 2]))
|
||||
var worst := 0.0
|
||||
for i in range(1, x.size()):
|
||||
worst = maxf(worst, absf(x[i] - x[i - 1]))
|
||||
chk("%s läuft nahtlos" % n, kink < worst * 1.5,
|
||||
"Knick %.4f, größter Schritt %.4f" % [kink, worst])
|
||||
var sq := _samples("squeal")
|
||||
var h: int = sq.size() / 2
|
||||
var a := _zcr(sq, 0, h)
|
||||
var b := _zcr(sq, h, sq.size())
|
||||
chk("Quietschen bleibt auf Tonhöhe", absf(a - b) / maxf(a, 0.0001) < 0.15,
|
||||
"Nulldurchgänge %.3f vorne, %.3f hinten" % [a, b])
|
||||
|
||||
func _samples(name: String) -> PackedFloat32Array:
|
||||
var d: PackedByteArray = (main.audio.sounds[name] as AudioStreamWAV).data
|
||||
var out := PackedFloat32Array()
|
||||
out.resize(d.size() / 2)
|
||||
for i in out.size():
|
||||
out[i] = d.decode_s16(i * 2) / 32768.0
|
||||
return out
|
||||
|
||||
## Sekunde, bis der Klang unter 4 % seines Höchstwerts fällt.
|
||||
func _tail(x: PackedFloat32Array) -> float:
|
||||
var peak := 0.0
|
||||
for v in x:
|
||||
peak = maxf(peak, absf(v))
|
||||
var last := 0
|
||||
for i in x.size():
|
||||
if absf(x[i]) > peak * 0.04:
|
||||
last = i
|
||||
return float(last) / 22050.0
|
||||
|
||||
func _zcr(x: PackedFloat32Array, a: int, b: int) -> float:
|
||||
var c := 0
|
||||
for i in range(a + 1, b):
|
||||
if (x[i] >= 0.0) != (x[i - 1] >= 0.0):
|
||||
c += 1
|
||||
return float(c) / float(maxi(b - a, 1))
|
||||
|
||||
## Sucht einen zweiten, deutlich getrennten Schlag: eine Spitze im
|
||||
## Bereich 0,10-0,25 s, davor ein echter Einbruch der Hüllkurve.
|
||||
func _double_hit(x: PackedFloat32Array) -> Dictionary:
|
||||
var win := 66
|
||||
var env := PackedFloat32Array()
|
||||
env.resize(x.size() / win)
|
||||
var peak := 0.0
|
||||
for i in env.size():
|
||||
var m := 0.0
|
||||
for k in win:
|
||||
m = maxf(m, absf(x[i * win + k]))
|
||||
env[i] = m
|
||||
peak = maxf(peak, m)
|
||||
var a: int = int(0.10 * 22050.0 / win)
|
||||
var b: int = mini(int(0.25 * 22050.0 / win), env.size() - 1)
|
||||
var best := 0.0
|
||||
var best_i := a
|
||||
for i in range(a, b + 1):
|
||||
if env[i] > best:
|
||||
best = env[i]
|
||||
best_i = i
|
||||
var dip := peak
|
||||
for i in range(int(0.03 * 22050.0 / win), best_i):
|
||||
dip = minf(dip, env[i])
|
||||
var ok: bool = best > peak * 0.22 and dip < best * 0.6
|
||||
return {"ok": ok, "at": float(best_i * win) / 22050.0, "dip": dip / maxf(best, 0.0001)}
|
||||
Reference in New Issue
Block a user