"""Gitea #51 -- audit which sounds the LoopAtWill reclassification actually changes. The old rule was `AL_LOOPING = (sample != ForceStatic)`, which made every one of the 224 non-ForceStatic samples an OpenAL source that never ends. The new rule defers a LoopAtWill sample to the SOURCE's authored AudioRenderType (streamed from AUDIO*.RES), which is what the enum comment says it means. This run drives a normal solo session -- engine, walking, weapons, coolant flush -- with BT_LOOP_AUDIT=1 and tabulates every sound by (sample flag, source type) so the reclassification is measured, not assumed. The load-bearing question: * do the sustained loops (engine, wind) stay loop=1? <- must be YES * which sounds flip [WAS 1, NOW 0]? <- must all be one-shots Kills only the PID it spawns. """ import ctypes import os import re import subprocess import sys import time from collections import Counter KEYUP = 0x0002 VK = {"W": 0x57, "H": 0x48, "C": 0x43, "SPACE": 0x20} user32 = ctypes.windll.user32 REPO = r"C:\git\bt411" LOG = os.path.join(REPO, "scratchpad_loopaudit.log") if os.path.exists(LOG): os.remove(LOG) env = dict(os.environ) env.update({ "BT_START_INSIDE": "1", "BT_KEY_NOFOCUS": "1", "BT_DEV_GAUGES": "1", "BT_LOOP_AUDIT": "1", "BT_AUTOFIRE": "1", # exercise weapon one-shots "BT_LOG": LOG, }) proc = subprocess.Popen( [os.path.join(REPO, "build", "Release", "btl4.exe"), "-egg", "LAST.EGG"], cwd=os.path.join(REPO, "content"), env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print("pid", proc.pid) def hold(name, secs): vk = VK[name] user32.keybd_event(vk, 0, 0, 0) time.sleep(secs) user32.keybd_event(vk, 0, KEYUP, 0) time.sleep(0.3) try: deadline = time.time() + 240 while time.time() < deadline: if os.path.exists(LOG) and "first frame" in open(LOG, errors="replace").read(): break time.sleep(2) else: print("FAIL: never reached the mission") proc.terminate() sys.exit(2) time.sleep(6) hold("W", 4.0) # throttle up -> engine + footfalls hold("H", 0.6) # coolant flush hold("C", 0.2) # valve hold("H", 0.6) # flush again time.sleep(8) finally: proc.terminate() time.sleep(1) t = open(LOG, errors="replace").read() if os.path.exists(LOG) else "" lines = re.findall(r"^\[loop-audit\].*$", t, re.M) combos = Counter() flipped = {} kept = {} for l in lines: m = re.search(r"file=(\S+) sample=(\S+) source=(\S+) -> loop=(\d)", l) if not m: continue f, samp, src, lp = m.groups() combos[(samp, src, lp)] += 1 if "[WAS 1, NOW 0]" in l: flipped[f] = (samp, src) elif lp == "1": kept[f] = (samp, src) print("\n=============== RESULT ===============") print("[loop-audit] events:", len(lines)) print("\nclassification matrix (sample flag x source type -> loop):") for (samp, src, lp), n in sorted(combos.items(), key=lambda kv: -kv[1]): print(" %-12s x %-9s -> loop=%s (%d events)" % (samp, src, lp, n)) print("\nSTILL LOOPING (loop=1) -- these must be genuine sustained loops:", len(kept)) for f, (samp, src) in sorted(kept.items()): print(" %-30s %s / %s" % (f, samp, src)) print("\nRECLASSIFIED to one-shot [WAS 1, NOW 0]:", len(flipped)) for f, (samp, src) in sorted(flipped.items()): print(" %-30s %s / %s" % (f, samp, src)) engine_kept = [f for f in kept if "Engine" in f or "Wind" in f] print("\nsustained engine/wind loops preserved:", engine_kept if engine_kept else "NONE -- INVESTIGATE") print("\nVERDICT:", "measured -- review the two lists above" if lines else "no [loop-audit] output (gate not firing?)") sys.exit(0)