KoshiMazaki commited on
Commit
e088a4d
·
verified ·
1 Parent(s): e3f7487

scripts: add measure_timing.py

Browse files
Files changed (1) hide show
  1. scripts/measure_timing.py +86 -0
scripts/measure_timing.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Envelope cross-correlation between a dry reference and its a2a render.
2
+
3
+ Reproduces the timing measurement published in the AKUSPACE model card, so new
4
+ examples can be added to that table on the same basis rather than asserted.
5
+
6
+ Method: amplitude envelope of each signal in 2 ms RMS windows, mean-removed and
7
+ normalised, cross-correlated over +/-250 ms. Reports the offset of peak
8
+ correlation and the peak value.
9
+
10
+ Reading the result: peak r measures how much the envelope CHANGED, and reverb
11
+ changes it by design — most on transient-dense material, where the tail fills
12
+ the gaps between hits. A low r on percussion is the effect working, not drift.
13
+ The offset is the timing claim; r is not a quality score.
14
+ """
15
+
16
+ import argparse
17
+ import subprocess
18
+ import sys
19
+ import wave
20
+ from pathlib import Path
21
+
22
+ import numpy as np
23
+
24
+ WIN_MS = 2.0
25
+ MAX_LAG_MS = 250.0
26
+
27
+
28
+ def load_mono(path: Path, sr: int = 48000) -> np.ndarray:
29
+ """Decode anything ffmpeg reads into mono float at sr."""
30
+ out = subprocess.run(
31
+ ["ffmpeg", "-v", "error", "-i", str(path), "-ac", "1", "-ar", str(sr),
32
+ "-f", "wav", "-c:a", "pcm_s16le", "-"],
33
+ capture_output=True, check=True).stdout
34
+ import io
35
+ with wave.open(io.BytesIO(out), "rb") as w:
36
+ raw = w.readframes(w.getnframes())
37
+ return np.frombuffer(raw, dtype="<i2").astype(np.float64) / 32768.0
38
+
39
+
40
+ def envelope(x: np.ndarray, sr: int) -> np.ndarray:
41
+ n = max(1, int(sr * WIN_MS / 1000.0))
42
+ trimmed = x[: len(x) - len(x) % n]
43
+ return np.sqrt((trimmed.reshape(-1, n) ** 2).mean(axis=1) + 1e-12)
44
+
45
+
46
+ def xcorr(a: np.ndarray, b: np.ndarray, max_lag: int) -> tuple[int, float]:
47
+ n = min(len(a), len(b))
48
+ a, b = a[:n], b[:n]
49
+ a = (a - a.mean()) / (a.std() or 1e-12)
50
+ b = (b - b.mean()) / (b.std() or 1e-12)
51
+ best_lag, best_r = 0, -2.0
52
+ for lag in range(-max_lag, max_lag + 1):
53
+ if lag < 0:
54
+ x, y = a[-lag:], b[: n + lag]
55
+ elif lag > 0:
56
+ x, y = a[: n - lag], b[lag:]
57
+ else:
58
+ x, y = a, b
59
+ if len(x) < 10:
60
+ continue
61
+ r = float((x * y).mean())
62
+ if r > best_r:
63
+ best_r, best_lag = r, lag
64
+ return best_lag, best_r
65
+
66
+
67
+ def main() -> int:
68
+ p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
69
+ p.add_argument("--dry", required=True)
70
+ p.add_argument("--wet", nargs="+", required=True)
71
+ p.add_argument("--sr", type=int, default=48000)
72
+ a = p.parse_args()
73
+
74
+ dry = envelope(load_mono(Path(a.dry), a.sr), a.sr)
75
+ max_lag = int(MAX_LAG_MS / WIN_MS)
76
+ print(f"{'example':38s} {'offset':>9s} {'peak r':>8s}")
77
+ for w in a.wet:
78
+ wet = envelope(load_mono(Path(w), a.sr), a.sr)
79
+ lag, r = xcorr(dry, wet, max_lag)
80
+ # positive lag = wet later than dry; report with the card's sign convention
81
+ print(f"{Path(w).stem:38s} {lag * WIN_MS:+8.0f}ms {r:8.2f}")
82
+ return 0
83
+
84
+
85
+ if __name__ == "__main__":
86
+ sys.exit(main())