Tools / src /video_editor /loudness_normalize.py
jebin2's picture
refactor: Centralize logger import to src.logger_config across various modules.
f20025d
import subprocess
import json
import re
from pathlib import Path
from src.logger_config import logger
def normalize_loudness(
input_path: str,
target_lufs: float = -10.0, # Middle of -12 to -8 range
target_tp: float = -1.0, # Your requirement
target_lra: float = 7.0, # More realistic than 2
overwrite: bool = True
):
"""
Two-pass EBU R128 loudness normalization with custom target LUFS and true peak
+ Verification pass to print final LUFS/LRA/TP values.
Args:
target_lufs: Target integrated loudness (-12 to -8 LUFS recommended)
target_tp: Target true peak in dBTP (-1.0 dB as per requirement)
target_lra: Target loudness range (7 is typical, 11 for dynamic content)
"""
input_path = Path(input_path)
stem = input_path.stem
output_path = Path(f"/tmp/{stem}.mp4")
if output_path.exists() and overwrite:
output_path.unlink()
# --- 1️⃣ First pass: analyze loudness ---
logger.debug("πŸ” Step 1: Analyzing loudness...")
cmd_analysis = [
"ffmpeg",
"-hide_banner", "-y",
"-i", str(input_path),
"-af", f"loudnorm=I={target_lufs}:TP={target_tp}:LRA={target_lra}:print_format=json",
"-f", "null", "-"
]
result = subprocess.run(cmd_analysis, capture_output=True, text=True)
match = re.search(r"\{[\s\S]*\}", result.stderr)
if not match:
raise RuntimeError("❌ Failed to parse loudnorm JSON output.")
loudnorm_data = json.loads(match.group(0))
logger.debug("πŸ“Š Analysis data:")
logger.debug(json.dumps(loudnorm_data, indent=4))
# --- 2️⃣ Second pass: apply normalization ---
logger.debug("\n🎚️ Step 2: Applying normalization...")
ln = loudnorm_data
loudnorm_filter = (
f"loudnorm=I={target_lufs}:TP={target_tp}:LRA={target_lra}:"
f"measured_I={ln['input_i']}:measured_TP={ln['input_tp']}:"
f"measured_LRA={ln['input_lra']}:measured_thresh={ln['input_thresh']}:"
f"offset={ln['target_offset']}:linear=true:print_format=summary"
)
cmd_apply = [
"ffmpeg",
"-hide_banner", "-y" if overwrite else "-n",
"-i", str(input_path),
"-af", loudnorm_filter,
"-c:v", "copy",
"-c:a", "aac",
"-b:a", "192k",
str(output_path)
]
subprocess.run(cmd_apply, check=True)
logger.debug(f"βœ… Normalized file saved at: {output_path}")
# --- 3️⃣ Third pass: verify final LUFS/LRA/TP ---
logger.debug("\nπŸ“Š Step 3: Verifying final loudness...")
cmd_verify = [
"ffmpeg",
"-hide_banner",
"-i", str(output_path),
"-af", f"loudnorm=I={target_lufs}:TP={target_tp}:LRA={target_lra}:print_format=json",
"-f", "null", "-"
]
result_verify = subprocess.run(cmd_verify, capture_output=True, text=True)
match_verify = re.search(r"\{[\s\S]*\}", result_verify.stderr)
if match_verify:
verify_data = json.loads(match_verify.group(0))
logger.debug("\nβœ… Final Loudness Stats:")
logger.debug(f"Integrated Loudness (LUFS): {verify_data['input_i']}")
logger.debug(f"True Peak (dBTP): {verify_data['input_tp']}")
logger.debug(f"Loudness Range (LRA): {verify_data['input_lra']}")
else:
logger.warning("⚠️ Could not extract final loudness stats.")
return output_path
# Example usage
if __name__ == "__main__":
# For -8 LUFS (louder): target_lufs=-8
# For -12 LUFS (quieter): target_lufs=-12
# For middle ground: target_lufs=-10
normalize_loudness("output_normalized.mp4")