"""Measure audio with ffmpeg: RMS and peak per 50 ms window. The checker and the assembler both use this.""" import math import os import re import subprocess import tempfile def ffmpeg(args): r = subprocess.run(["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y"] + args, capture_output=True, text=True) if r.returncode != 0: raise RuntimeError("ffmpeg failed: " + r.stderr.strip()[-500:]) def probe(path): out = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=codec_name,sample_rate,channels,bit_rate:format=duration", "-of", "default=nw=1", path], capture_output=True, text=True, check=True).stdout return dict(line.split("=", 1) for line in out.strip().splitlines()) def windows(path): """[(rms_db, peak_db)] for consecutive 50 ms windows of the decoded file.""" sr = int(probe(path)["sample_rate"]) n = sr // 20 with tempfile.TemporaryDirectory() as tmp: rms_file, peak_file = os.path.join(tmp, "rms.txt"), os.path.join(tmp, "peak.txt") af = ("asetnsamples=n=%d:p=0,astats=metadata=1:reset=1," "ametadata=print:key=lavfi.astats.Overall.RMS_level:file=%s," "ametadata=print:key=lavfi.astats.Overall.Peak_level:file=%s" % (n, rms_file, peak_file)) r = subprocess.run(["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-i", path, "-af", af, "-f", "null", "-"], capture_output=True, text=True) if r.returncode != 0: raise RuntimeError("ffmpeg failed: " + r.stderr.strip()[-500:]) read = lambda p, key: [float(v) for v in re.findall(key + r"=(-?[\d.]+|-inf|inf)", open(p).read())] rms, peak = read(rms_file, "RMS_level"), read(peak_file, "Peak_level") return list(zip(rms, peak)) def db_mean_power(values): """Overall level in dB of equal-length windows given as dB RMS.""" if not values: return float("-inf") p = sum(10 ** (v / 10) for v in values) / len(values) return 10 * math.log10(p) if p > 0 else float("-inf") def overall(path): """(rms_db, peak_db) of the whole file.""" w = windows(path) return db_mean_power([r for r, _ in w]), max(p for _, p in w)