"""생성기와 독립된 새 ebur128 프로세스가 정확한 최종 WAV의 릴리스 계기다.""" import hashlib import json import math from pathlib import Path import re import subprocess import unittest # manifest가 목표를 바꾸어 실패를 숨길 수 없도록 수용값을 독립적으로 고정한다. TARGET = -32.5 CEILING = -6.0 ROOT = Path(__file__).resolve().parents[2] def independent_measure(path): """정규화 필터의 자기 보고 없이 저장된 전체 PCM을 전용 EBU 계기로 측정한다.""" command = ["ffmpeg", "-nostdin", "-hide_banner", "-i", str(path), "-af", "ebur128=peak=true", "-f", "null", "-"] result = subprocess.run(command, check=True, capture_output=True, text=True, timeout=30) summary = result.stderr.rsplit("Summary:", 1)[1] values = { "integratedLUFS": float(re.search(r"I:\s+([-\d.]+) LUFS", summary)[1]), "truePeakDBTP": float(re.search(r"Peak:\s+([-\d.]+) dBFS", summary)[1]), "loudnessRangeLU": float(re.search(r"LRA:\s+([-\d.]+) LU", summary)[1]), "thresholdLUFS": float(re.search(r"Threshold:\s+([-\d.]+) LUFS", summary)[1]), } if not all(math.isfinite(v) for v in values.values()): raise ValueError("non-finite ebur128 measurement") return values class FinalLoudness(unittest.TestCase): """8 WAV의 동등성 assertion은 계기 불일치를 이유로 면제하지 않는다.""" @classmethod def setUpClass(cls): cls.rows = {p.stem: independent_measure(p) for p in sorted((ROOT / "public/ambience").glob("*.wav"))} def test_final_wavs_meet_fixed_loudness_and_true_peak(self): self.assertEqual(len(self.rows), 8) spread = max(r["integratedLUFS"] for r in self.rows.values()) - min(r["integratedLUFS"] for r in self.rows.values()) print(json.dumps({"measurementFilter": "ebur128=peak=true", "targetLUFS": TARGET, "truePeakCeilingDBTP": CEILING, "spreadLU": round(spread, 2), "files": self.rows}, indent=2), flush=True) for name, row in self.rows.items(): with self.subTest(environment=name): self.assertLessEqual(abs(row["integratedLUFS"] - TARGET), 0.2 + 1e-9) self.assertLessEqual(row["truePeakDBTP"], CEILING) # 삭제되었던 ebur128 integrated 동등성 검사를 릴리스 필수 조건으로 복원한다. self.assertLessEqual(spread, 0.4 + 1e-9) def test_manifest_records_linear_gain_and_exact_ebur128_measurements(self): manifest = json.loads((ROOT / "public/ambience/manifest.json").read_text()) settings = manifest["normalization"] for key, value in {"targetIntegratedLUFS": TARGET, "truePeakCeilingDBTP": CEILING, "internalTruePeakCeilingDBTP": -6.2, "toleranceLU": 0.2, "maximumSpreadLU": 0.4, "measurementFilter": "ebur128=peak=true", "maximumIterations": 3}.items(): self.assertEqual(settings[key], value) self.assertIn("ffmpeg version", manifest["toolVersions"]["ffmpeg"]) for entry in manifest["files"]: norm = entry["conversion"]["loudness"] self.assertEqual(norm["measurementFilter"], "ebur128=peak=true") self.assertRegex(norm["fadedPcmSha256"], r"^[a-f0-9]{64}$") self.assertGreaterEqual(len(norm["iterations"]), 1) self.assertLessEqual(len(norm["iterations"]), 3) gain = TARGET - norm["input"]["integratedLUFS"] for iteration in norm["iterations"]: self.assertAlmostEqual(iteration["gainDB"], gain, places=9) self.assertEqual(iteration["filter"], f"volume={gain:.12f}dB:precision=double") self.assertLessEqual(norm["input"]["truePeakDBTP"] + gain, -6.2 + 1e-9) gain += TARGET - iteration["output"]["integratedLUFS"] self.assertEqual(norm["final"], norm["iterations"][-1]["output"]) # manifest 수치는 새 프로세스 실측과 정확히 같아야 한다. self.assertEqual(norm["final"], self.rows[entry["environmentId"]]) self.assertEqual(entry["sha256"], hashlib.sha256((ROOT / "public/ambience" / entry["file"]).read_bytes()).hexdigest())