fix: normalize ambience loudness for v0.2.5
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""생성기와 독립된 새 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())
|
||||
Reference in New Issue
Block a user