246 lines
8.0 KiB
GDScript
246 lines
8.0 KiB
GDScript
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)}
|