Files
skate-3-rust-engine/tools/recomp-trace/send_vectors.py
T
Hails 418dcecd41 Audio: interim cue tables retired, native water splash, published tools
- The interim cue tables and their opt-outs are gone; the native runtime is the only audio path. Old
  settings/audio.json files still load ("interim" / "native" ignored). Missing native data logs an error
  instead of falling back.
- Water splash from retail's mechanism (SFXObj_OffBoard start / update): Skate_Collisions 1187 / 1197 once
  per water stay, 1198 when the board is in water.
- The random interim riding bed is removed; riding collision and cloth sounds compared with the recomp
  (doc 11). Native output otherwise byte-identical (13 scenarios + 5 sessions).
- Published tools under tools/ (one folder per tool with a README; the recomp ones labelled for use with the
  research hooks), doc 14 and PR plan bucket L.
- Docs: doc 11 (retirement, bail density measured against the recomp, collision posts), doc 13 (usage guide).
2026-10-03 10:20:08 -06:00

58 lines
2.3 KiB
Python

"""Summarise retail per-channel send gains from a recomp audio trace (SEND lines).
SEND <ms> <owner> <object> <target> <channels> <cell0,cell1,...> (send hook; change-only)
The cells are the send's per-channel gains (pan matrix row x target). For the voice graph's final
6-channel send, cells/target is the panner output for that voice, so this checks a pan law:
power sum, LFE share, most common shapes.
Usage: py -3.13 tools/recomp-trace/send_vectors.py <trace.tsv> [channels=6] [min_target=0.05]
"""
import collections
import sys
def main() -> None:
if len(sys.argv) < 2 or sys.argv[1] in ('-h', '--help'):
sys.exit(__doc__)
path = sys.argv[1]
want = int(sys.argv[2]) if len(sys.argv) > 2 else 6
floor = float(sys.argv[3]) if len(sys.argv) > 3 else 0.05
sums, lfe, shapes, peaks = [], [], collections.Counter(), []
with open(path, encoding="utf-8", errors="replace") as fh:
for line in fh:
if not line.startswith("SEND\t"):
continue
f = line.rstrip("\n").split("\t")
if len(f) < 7 or not f[5].isdigit() or int(f[5]) != want:
continue
try:
target = float(f[4])
cells = [float(x) for x in f[6].split(",")]
except ValueError:
continue
if target < floor or len(cells) != want:
continue
r = [c / target for c in cells]
sums.append(sum(x * x for x in r))
peaks.append(max(r))
if want == 6:
lfe.append(r[3])
shapes[tuple(round(x, 3) for x in r)] += 1
def pct(xs, p):
xs = sorted(xs)
return xs[min(len(xs) - 1, int(p * len(xs)))] if xs else float("nan")
print(f"vectors {len(sums)} (channels {want}, target >= {floor})")
print("sum of squares p1/p50/p99: %.4f %.4f %.4f" % (pct(sums, .01), pct(sums, .5), pct(sums, .99)))
print("max cell p1/p50/p99: %.4f %.4f %.4f" % (pct(peaks, .01), pct(peaks, .5), pct(peaks, .99)))
if lfe:
print("LFE share p50/p99/max: %.4f %.4f %.4f" % (pct(lfe, .5), pct(lfe, .99), max(lfe)))
print("most common shapes (cells / target):")
for shape, n in shapes.most_common(30):
print(f" {n:6d} {shape}")
if __name__ == "__main__":
main()