166 lines
5.5 KiB
Python
Executable File
166 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""bit-exact verification — harness out vs plugin *_ref.wav, sample-level report.
|
|
|
|
Usage:
|
|
verify_bit_exact.py # sweep all *.rpp in TEST_ROOT (input=basename.wav)
|
|
verify_bit_exact.py --rpp comb_base # single case (uses comb_base.rpp + input wav + _ref.wav)
|
|
verify_bit_exact.py --mono # pipeline operates on channel-average (MEAN)
|
|
|
|
For each case `name`: read params from `name.rpp` (rpp_allparams), render
|
|
`<input>.wav -> /tmp/out_<name>.wav` via dsp/harness, compare samples against
|
|
`TEST_ROOT/<name>_ref.wav` (plugin output). Reports samples differing, max |dx|,
|
|
RMSE and first mismatching sample.
|
|
"""
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import wave
|
|
import struct
|
|
import glob
|
|
import json
|
|
|
|
HARNESS = "/home/m/re-tools/dsp/build/soothe2_harness"
|
|
TEST_ROOT = "/home/m/soothe-bt"
|
|
RPPMOD = os.path.join("/home/m/re-tools/handoff", "rpp_allparams.py")
|
|
MAX_FRAMES = None # set to int to compare only first N frames
|
|
|
|
def read_wav3(path):
|
|
with wave.open(path, 'rb') as w:
|
|
n = w.getnframes()
|
|
nc = w.getnchannels()
|
|
sw = w.getsampwidth()
|
|
extra = w.readframes(n)
|
|
if sw == 3:
|
|
raw = bytearray(extra)
|
|
# make int list
|
|
if len(raw) % 3:
|
|
raw += b'\x00' * (3 - len(raw) % 3)
|
|
vals = []
|
|
for i in range(0, len(raw), 3):
|
|
v = raw[i] | (raw[i+1] << 8) | (raw[i+2] << 16)
|
|
if v & 0x800000:
|
|
v -= 0x1000000
|
|
vals.append(v)
|
|
return vals, nc, sw
|
|
elif sw == 2:
|
|
vals = list(struct.unpack(f'<{len(extra)//2}h', extra))
|
|
return vals, nc, sw
|
|
raise ValueError(f"unsupported sampwidth {sw}")
|
|
|
|
def rpp_meta(name):
|
|
"""Return (input_path, ref_path, conf_path) from RPP project file."""
|
|
rpp = f"{TEST_ROOT}/{name}.rpp"
|
|
if not os.path.exists(rpp):
|
|
return None, None, None
|
|
src = ref = None
|
|
in_src = False
|
|
with open(rpp, errors='replace') as f:
|
|
for line in f:
|
|
if '<SOURCE WAVE' in line and src is None:
|
|
in_src = True
|
|
continue
|
|
if in_src and 'FILE "' in line and src is None:
|
|
i = line.find('FILE "')
|
|
src = line[i + 6:].split('"')[0]
|
|
in_src = False
|
|
if 'RENDER_FILE' in line and ref is None:
|
|
i = line.find('"')
|
|
if i >= 0:
|
|
ref = line[i + 1:].split('"')[0]
|
|
conf = f"/tmp/{name}.conf"
|
|
with open(conf, 'w') as cf:
|
|
subprocess.run([sys.executable, RPPMOD, "--flat", rpp],
|
|
stdout=cf, check=False)
|
|
if os.path.getsize(conf) == 0:
|
|
conf = None
|
|
return src, ref, conf
|
|
|
|
def first_mismatch(a, b, lim=50000):
|
|
for i in range(min(len(a), len(b), lim)):
|
|
if a[i] != b[i]:
|
|
return i
|
|
return -1
|
|
|
|
def compare(name, in_wav, mono, report=3):
|
|
src, ref, conf = rpp_meta(name)
|
|
if not conf:
|
|
return None, "params decode failed"
|
|
if not src or not os.path.exists(src):
|
|
return None, f"source wav missing: {src}"
|
|
if not ref or not os.path.exists(ref):
|
|
return None, f"ref wav missing: {ref}"
|
|
out = f"/tmp/out_{name}.wav"
|
|
|
|
cmd = [HARNESS, in_wav, out, conf]
|
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
if r.returncode != 0:
|
|
return None, f"harness rc={r.returncode}: {r.stderr[:200]}"
|
|
|
|
ra, rc, rw = read_wav3(ref)
|
|
oa, oc, ow = read_wav3(out)
|
|
if not ra or not oa:
|
|
return None, "empty wav"
|
|
if len(ra) < len(oa):
|
|
ra = ra[:len(oa)] + [0] * (len(oa) - len(ra))
|
|
|
|
if mono:
|
|
# combine channels into mono reference (L+R)/2 style not exact here:
|
|
# use channel-mean of both, compare only channel 0
|
|
ra = list(ra[0::rc])
|
|
oa = list(oa[0::oc])
|
|
rc = oc = 1
|
|
|
|
if MAX_FRAMES:
|
|
cut = MAX_FRAMES * rc
|
|
ra, oa = ra[:cut], oa[:cut]
|
|
|
|
n = min(len(ra), len(oa))
|
|
diff = sum(1 for i in range(n) if ra[i] != oa[i])
|
|
mx = max((abs(ra[i] - oa[i]) for i in range(n)), default=0)
|
|
if n:
|
|
rmse = (sum((ra[i] - oa[i]) ** 2 for i in range(n)) / n) ** 0.5
|
|
else:
|
|
rmse = 0.0
|
|
mm = first_mismatch(ra, oa)
|
|
return dict(frames=n, diff=diff, frac=diff / n if n else 1.0,
|
|
mx=mx, rmse=rmse, first=mm), None
|
|
|
|
def main():
|
|
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
|
mono = '--mono' in sys.argv[1:]
|
|
single = args[0] if args else None
|
|
if single:
|
|
names = [single]
|
|
else:
|
|
names = sorted(os.path.splitext(os.path.basename(p))[0]
|
|
for p in glob.glob(f"{TEST_ROOT}/*.rpp"))
|
|
print(f"{len(names)} cases ('--mono' = single channel)")
|
|
stat = dict(bit=0, nearbit=0, diff=0, skip=0)
|
|
|
|
for name in names:
|
|
src, ref, conf = rpp_meta(name)
|
|
if not os.path.exists(src) if src else True:
|
|
stat['skip'] += 1
|
|
continue
|
|
res, err = compare(name, src, mono)
|
|
if res is None:
|
|
print(f" SKIP {name}: {err}")
|
|
stat['skip'] += 1
|
|
continue
|
|
frac = res['frac']
|
|
if frac == 0.0:
|
|
stat['bit'] += 1
|
|
tag = "BIT-EXACT"
|
|
elif frac < 0.05:
|
|
stat['nearbit'] += 1
|
|
tag = "near"
|
|
else:
|
|
stat['diff'] += 1
|
|
tag = "DIFF"
|
|
print(f" [{tag:8s}] {name:28s} frames={res['frames']:7d} "
|
|
f"diff={res['diff']:7d} ({res['frac']*100:5.2f}%) "
|
|
f"max|dx|={res['mx']} rmse={res['rmse']:.1f} first@{res['first']}")
|
|
print("\n" + json.dumps(stat, indent=0))
|
|
|
|
if __name__ == "__main__":
|
|
main() |