From e785deabd06eae39a6051226df436b9993cfc75a Mon Sep 17 00:00:00 2001 From: Ahmet Sait Date: Mon, 9 Dec 2024 23:11:20 +0300 Subject: [PATCH] Implement `--safe-ebu` --- README.md | 6 ++++++ ffmpeg_normalize/__main__.py | 18 ++++++++++++++++++ ffmpeg_normalize/_ffmpeg_normalize.py | 3 +++ ffmpeg_normalize/_streams.py | 16 +++++++++++++++- 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fad862..8a23c19 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,12 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali If the measured loudness from the first pass is lower than the target loudness then normalization pass will be skipped for the measured audio source. +- `--safe-ebu MARGIN`: Safe EBU Margin (default: 0.0). + + Automatically lower EBU Integrated Loudness Target to prevent falling back to dynamic filtering. (default: 0.0) + + Makes sure target loudness is lower than measured loudness minus peak loudness (input_i - input_tp) by the given amount. A value of 0 means disabled. + - `--dual-mono`: Treat mono input files as "dual-mono". If a mono file is intended for playback on a stereo system, its EBU R128 measurement will be perceptually incorrect. If set, this option will compensate for this effect. Multi-channel input files are not affected by this option. diff --git a/ffmpeg_normalize/__main__.py b/ffmpeg_normalize/__main__.py index 6a51f30..fd795c6 100644 --- a/ffmpeg_normalize/__main__.py +++ b/ffmpeg_normalize/__main__.py @@ -247,6 +247,23 @@ def create_parser() -> argparse.ArgumentParser: ), ) + group_ebu.add_argument( + "--safe-ebu", + metavar="MARGIN", + type=float, + help=textwrap.dedent( + """\ + Automatically lower EBU Integrated Loudness Target to prevent falling + back to dynamic filtering. (default: 0.0) + + Makes sure target loudness is lower than measured loudness minus peak + loudness (input_i - input_tp) by the given amount. A value of 0 means + disabled. + """ + ), + default=0.0, + ) + group_ebu.add_argument( "--dual-mono", action="store_true", @@ -527,6 +544,7 @@ def _split_options(opts: str) -> list[str]: true_peak=cli_args.true_peak, offset=cli_args.offset, lower_only=cli_args.lower_only, + safe_ebu=cli_args.safe_ebu, dual_mono=cli_args.dual_mono, dynamic=cli_args.dynamic, audio_codec=cli_args.audio_codec, diff --git a/ffmpeg_normalize/_ffmpeg_normalize.py b/ffmpeg_normalize/_ffmpeg_normalize.py index 67ae5f8..899f726 100644 --- a/ffmpeg_normalize/_ffmpeg_normalize.py +++ b/ffmpeg_normalize/_ffmpeg_normalize.py @@ -61,6 +61,7 @@ class FFmpegNormalize: true_peak (float, optional): True peak. Defaults to -2.0. offset (float, optional): Offset. Defaults to 0.0. lower_only (bool, optional): Whether the audio should not increase in loudness. Defaults to False. + safe_ebu (float, optional): Safe EBU margin. Defaults to 0.0. dual_mono (bool, optional): Dual mono. Defaults to False. dynamic (bool, optional): Dynamic. Defaults to False. audio_codec (str, optional): Audio codec. Defaults to "pcm_s16le". @@ -98,6 +99,7 @@ def __init__( true_peak: float = -2.0, offset: float = 0.0, lower_only: bool = False, + safe_ebu: float = 0.0, dual_mono: bool = False, dynamic: bool = False, audio_codec: str = "pcm_s16le", @@ -169,6 +171,7 @@ def __init__( self.true_peak = check_range(true_peak, -9, 0, name="true_peak") self.offset = check_range(offset, -99, 99, name="offset") self.lower_only = lower_only + self.safe_ebu = safe_ebu # Ensure library user is passing correct types assert isinstance(dual_mono, bool), "dual_mono must be bool" diff --git a/ffmpeg_normalize/_streams.py b/ffmpeg_normalize/_streams.py index 09b715e..f601f31 100644 --- a/ffmpeg_normalize/_streams.py +++ b/ffmpeg_normalize/_streams.py @@ -481,10 +481,24 @@ def get_second_pass_opts_ebu(self) -> str: "Specify -ar/--sample-rate to override it." ) + target_level = self.ffmpeg_normalize.target_level + if self.ffmpeg_normalize.safe_ebu > 0: + safe_target = ( + self.loudness_statistics["ebu_pass1"]["input_i"] + - self.loudness_statistics["ebu_pass1"]["input_tp"] + + self.ffmpeg_normalize.true_peak + - self.ffmpeg_normalize.safe_ebu + ) + if safe_target < self.ffmpeg_normalize.target_level: + _logger.warning( + f"Using loudness target {safe_target} because --safe-ebu={self.ffmpeg_normalize.safe_ebu}.", + ) + target_level = safe_target + stats = self.loudness_statistics["ebu_pass1"] opts = { - "i": self.media_file.ffmpeg_normalize.target_level, + "i": target_level, "lra": self.media_file.ffmpeg_normalize.loudness_range_target, "tp": self.media_file.ffmpeg_normalize.true_peak, "offset": self._constrain(