Files
midi-playlist/tools/check_wav.py
T

54 lines
1.9 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
check_wav.py — sanity-check a rendered WAV: duration, peak (clipping),
RMS loudness, and dead-air (long silent stretches that usually mean a
MIDI track failed to render).
Usage: python3 tools/check_wav.py file.wav [expected_sec]
Exit 0 = pass, 1 = fail. Designed for CI / cron use.
"""
import sys, wave, struct
def analyse(path):
w = wave.open(path)
n, ch, sw = w.getnframes(), w.getnchannels(), w.getsampwidth()
rate = w.getframerate()
assert sw == 2, "expected 16-bit PCM, got %d-bit" % (sw * 8)
samples = struct.unpack("<%dh" % (n * ch), w.readframes(n))
peak = max(abs(s) for s in samples)
rms = (sum(s * s for s in samples) / len(samples)) ** 0.5
# dead air: longest run of frames where every channel < 64
silent_run = worst = 0
for i in range(0, len(samples), ch):
if max(abs(s) for s in samples[i:i + ch]) < 64:
silent_run += 1
worst = max(worst, silent_run)
else:
silent_run = 0
return {"sec": n / rate, "rate": rate, "peak": peak, "rms": rms,
"max_silence_sec": worst / rate}
def main():
path = sys.argv[1]
expected = float(sys.argv[2]) if len(sys.argv) > 2 else None
a = analyse(path)
fails = []
if a["peak"] >= 32767:
fails.append("CLIPPING (peak %d)" % a["peak"])
if a["rms"] < 300:
fails.append("too quiet (rms %.0f)" % a["rms"])
if a["max_silence_sec"] > 8:
fails.append("dead air: %.1fs silent stretch" % a["max_silence_sec"])
if expected and abs(a["sec"] - expected) > expected * 0.15 + 2:
fails.append("duration %.1fs, expected ~%.1fs" % (a["sec"], expected))
print("%s: %.1fs peak=%d rms=%.0f max_silence=%.1fs" %
(path, a["sec"], a["peak"], a["rms"], a["max_silence_sec"]))
for f in fails:
print(" FAIL:", f)
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main())