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)}