ffmpeg-normalize 1.30.0__py2.py3-none-any.whl → 1.31.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -201,9 +201,7 @@ def create_parser() -> argparse.ArgumentParser:
201
201
  help=textwrap.dedent(
202
202
  """\
203
203
  Keep input loudness range above loudness range target.
204
- - `LOUDNESS_RANGE_TARGET` for input loudness range `<= LOUDNESS_RANGE_TARGET` or
205
- - keep input loudness range target above `LOUDNESS_RANGE_TARGET`.
206
- as alternative to `--keep-loudness-range-target` to allow for linear normalization.
204
+ Can be used as an alternative to `--keep-loudness-range-target` to allow for linear normalization.
207
205
  """
208
206
  ),
209
207
  )
@@ -249,6 +247,20 @@ def create_parser() -> argparse.ArgumentParser:
249
247
  ),
250
248
  )
251
249
 
250
+ group_ebu.add_argument(
251
+ "--auto-lower-loudness-target",
252
+ action="store_true",
253
+ help=textwrap.dedent(
254
+ """\
255
+ Automatically lower EBU Integrated Loudness Target to prevent falling
256
+ back to dynamic filtering.
257
+
258
+ Makes sure target loudness is lower than measured loudness minus peak
259
+ loudness (input_i - input_tp) by a small amount (0.1 LUFS).
260
+ """
261
+ ),
262
+ )
263
+
252
264
  group_ebu.add_argument(
253
265
  "--dual-mono",
254
266
  action="store_true",
@@ -529,6 +541,7 @@ def main() -> None:
529
541
  true_peak=cli_args.true_peak,
530
542
  offset=cli_args.offset,
531
543
  lower_only=cli_args.lower_only,
544
+ auto_lower_loudness_target=cli_args.auto_lower_loudness_target,
532
545
  dual_mono=cli_args.dual_mono,
533
546
  dynamic=cli_args.dynamic,
534
547
  audio_codec=cli_args.audio_codec,
@@ -7,7 +7,7 @@ import shlex
7
7
  import subprocess
8
8
  from platform import system
9
9
  from shutil import which
10
- from typing import Iterator
10
+ from typing import Iterator, Any
11
11
 
12
12
  from ffmpeg_progress_yield import FfmpegProgress
13
13
 
@@ -128,12 +128,12 @@ class CommandRunner:
128
128
  return self.output
129
129
 
130
130
 
131
- def dict_to_filter_opts(opts: dict[str, object]) -> str:
131
+ def dict_to_filter_opts(opts: dict[str, Any]) -> str:
132
132
  """
133
133
  Convert a dictionary to a ffmpeg filter option string
134
134
 
135
135
  Args:
136
- opts (dict[str, object]): Dictionary of options
136
+ opts (dict[str, Any]): Dictionary of options
137
137
 
138
138
  Returns:
139
139
  str: Filter option string
@@ -61,6 +61,7 @@ class FFmpegNormalize:
61
61
  true_peak (float, optional): True peak. Defaults to -2.0.
62
62
  offset (float, optional): Offset. Defaults to 0.0.
63
63
  lower_only (bool, optional): Whether the audio should not increase in loudness. Defaults to False.
64
+ auto_lower_loudness_target (bool, optional): Automatically lower EBU Integrated Loudness Target.
64
65
  dual_mono (bool, optional): Dual mono. Defaults to False.
65
66
  dynamic (bool, optional): Dynamic. Defaults to False.
66
67
  audio_codec (str, optional): Audio codec. Defaults to "pcm_s16le".
@@ -98,6 +99,7 @@ class FFmpegNormalize:
98
99
  true_peak: float = -2.0,
99
100
  offset: float = 0.0,
100
101
  lower_only: bool = False,
102
+ auto_lower_loudness_target: bool = False,
101
103
  dual_mono: bool = False,
102
104
  dynamic: bool = False,
103
105
  audio_codec: str = "pcm_s16le",
@@ -169,6 +171,7 @@ class FFmpegNormalize:
169
171
  self.true_peak = check_range(true_peak, -9, 0, name="true_peak")
170
172
  self.offset = check_range(offset, -99, 99, name="offset")
171
173
  self.lower_only = lower_only
174
+ self.auto_lower_loudness_target = auto_lower_loudness_target
172
175
 
173
176
  # Ensure library user is passing correct types
174
177
  assert isinstance(dual_mono, bool), "dual_mono must be bool"
@@ -177,8 +177,7 @@ class MediaFile:
177
177
  and len(self.streams["audio"].values()) > 1
178
178
  ):
179
179
  _logger.warning(
180
- "Output file only supports one stream. "
181
- "Keeping only first audio stream."
180
+ "Output file only supports one stream. Keeping only first audio stream."
182
181
  )
183
182
  first_stream = list(self.streams["audio"].values())[0]
184
183
  self.streams["audio"] = {first_stream.stream_id: first_stream}
@@ -281,7 +280,7 @@ class MediaFile:
281
280
  skip_normalization = True
282
281
 
283
282
  if skip_normalization:
284
- _logger.warn(
283
+ _logger.warning(
285
284
  f"Stream {audio_stream.stream_id} had measured input loudness lower than target, skipping normalization."
286
285
  )
287
286
  normalization_filter = "acopy"
@@ -390,7 +389,12 @@ class MediaFile:
390
389
 
391
390
  # other audio options (if any)
392
391
  if self.ffmpeg_normalize.audio_bitrate:
393
- cmd.extend(["-b:a", str(self.ffmpeg_normalize.audio_bitrate)])
392
+ if self.ffmpeg_normalize.audio_codec == "libvorbis":
393
+ # libvorbis takes just a "-b" option, for some reason
394
+ # https://github.com/slhck/ffmpeg-normalize/issues/277
395
+ cmd.extend(["-b", str(self.ffmpeg_normalize.audio_bitrate)])
396
+ else:
397
+ cmd.extend(["-b:a", str(self.ffmpeg_normalize.audio_bitrate)])
394
398
  if self.ffmpeg_normalize.sample_rate:
395
399
  cmd.extend(["-ar", str(self.ffmpeg_normalize.sample_rate)])
396
400
  if self.ffmpeg_normalize.audio_channels:
@@ -167,7 +167,7 @@ class AudioStream(MediaStream):
167
167
  }
168
168
  return stats
169
169
 
170
- def set_second_pass_stats(self, stats: EbuLoudnessStatistics):
170
+ def set_second_pass_stats(self, stats: EbuLoudnessStatistics) -> None:
171
171
  """
172
172
  Set the EBU loudness statistics for the second pass.
173
173
 
@@ -481,10 +481,24 @@ class AudioStream(MediaStream):
481
481
  "Specify -ar/--sample-rate to override it."
482
482
  )
483
483
 
484
+ target_level = self.ffmpeg_normalize.target_level
485
+ if self.ffmpeg_normalize.auto_lower_loudness_target:
486
+ safe_target = (
487
+ self.loudness_statistics["ebu_pass1"]["input_i"]
488
+ - self.loudness_statistics["ebu_pass1"]["input_tp"]
489
+ + self.ffmpeg_normalize.true_peak
490
+ - 0.1
491
+ )
492
+ if safe_target < self.ffmpeg_normalize.target_level:
493
+ target_level = safe_target
494
+ _logger.warning(
495
+ f"Using loudness target {target_level} because --auto-lower-loudness-target given.",
496
+ )
497
+
484
498
  stats = self.loudness_statistics["ebu_pass1"]
485
499
 
486
500
  opts = {
487
- "i": self.media_file.ffmpeg_normalize.target_level,
501
+ "i": target_level,
488
502
  "lra": self.media_file.ffmpeg_normalize.loudness_range_target,
489
503
  "tp": self.media_file.ffmpeg_normalize.true_peak,
490
504
  "offset": self._constrain(
@@ -1 +1 @@
1
- __version__ = "1.30.0"
1
+ __version__ = "1.31.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ffmpeg-normalize
3
- Version: 1.30.0
3
+ Version: 1.31.1
4
4
  Summary: Normalize audio via ffmpeg
5
5
  Home-page: https://github.com/slhck/ffmpeg-normalize
6
6
  Author: Werner Robitza
@@ -58,6 +58,7 @@ Read on for more info.
58
58
  - [Requirements](#requirements)
59
59
  - [ffmpeg](#ffmpeg)
60
60
  - [Installation](#installation)
61
+ - [Shell Completions](#shell-completions)
61
62
  - [Usage with Docker](#usage-with-docker)
62
63
  - [High LeveL Introduction](#high-level-introduction)
63
64
  - [Basic Usage](#basic-usage)
@@ -134,6 +135,54 @@ Or download this repository, then run `pip3 install .`.
134
135
 
135
136
  To later upgrade to the latest version, run `pip3 install --upgrade ffmpeg-normalize`.
136
137
 
138
+ ### Shell Completions
139
+
140
+ This tool provides shell completions for bash and zsh. To install them:
141
+
142
+ <!--
143
+ Note to self: Generate the shtab ones with:
144
+
145
+ shtab --shell=bash -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.bash
146
+ shtab --shell=zsh -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.zsh
147
+
148
+ but these are not properly working yet.
149
+ -->
150
+
151
+ #### Bash
152
+
153
+ If you have [`bash-completion`](https://github.com/scop/bash-completion) installed, you can just copy your new completion script to the `/usr/local/etc/bash_completion.d` directory.
154
+
155
+ ```bash
156
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
157
+ -o /usr/local/etc/bash_completion.d/ffmpeg-normalize
158
+ ```
159
+
160
+ Without bash-completion, you can manually install the completion script:
161
+
162
+ ```bash
163
+ # create completions directory if it doesn't exist
164
+ mkdir -p ~/.bash_completions.d
165
+
166
+ # download and install completion script
167
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
168
+ -o ~/.bash_completions.d/ffmpeg-normalize
169
+
170
+ # source it in your ~/.bashrc
171
+ echo 'source ~/.bash_completions.d/ffmpeg-normalize' >> ~/.bashrc
172
+ ```
173
+
174
+ #### Zsh
175
+
176
+ Download the completion script and place it in the default `site-functions` directory:
177
+
178
+ ```bash
179
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize.zsh \
180
+ -o /usr/local/share/zsh/site-functions/
181
+ ```
182
+
183
+ You may choose any other directory that is in your `$FPATH` variable.
184
+ Make sure your `.zshrc` file contains `autoload -Uz compinit && compinit`.
185
+
137
186
  ## Usage with Docker
138
187
 
139
188
  You can use the pre-built image from Docker Hub:
@@ -285,10 +334,7 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
285
334
 
286
335
  - `--keep-lra-above-loudness-range-target`: Keep input loudness range above loudness range target.
287
336
 
288
- - `LOUDNESS_RANGE_TARGET` for input loudness range `<= LOUDNESS_RANGE_TARGET` or
289
- - keep input loudness range target above `LOUDNESS_RANGE_TARGET`.
290
-
291
- as alternative to `--keep-loudness-range-target` to allow for linear normalization.
337
+ Can be used as an alternative to `--keep-loudness-range-target` to allow for linear normalization.
292
338
 
293
339
  - `-tp TRUE_PEAK, --true-peak TRUE_PEAK`: EBU Maximum True Peak in dBTP (default: -2.0).
294
340
 
@@ -300,6 +346,16 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
300
346
 
301
347
  Range is -99.0 - +99.0.
302
348
 
349
+ - `--lower-only`: Whether the audio should not increase in loudness.
350
+
351
+ 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.
352
+
353
+ - `--auto-lower-loudness-target`: Automatically lower EBU Integrated Loudness Target.
354
+
355
+ Automatically lower EBU Integrated Loudness Target to prevent falling back to dynamic filtering.
356
+
357
+ Makes sure target loudness is lower than measured loudness minus peak loudness (input_i - input_tp) by a small amount.
358
+
303
359
  - `--dual-mono`: Treat mono input files as "dual-mono".
304
360
 
305
361
  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.
@@ -308,7 +364,7 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
308
364
 
309
365
  Instead of applying linear EBU R128 normalization, choose a dynamic normalization. This is not usually recommended.
310
366
 
311
- Dynamic mode will automatically change the sample rate to 192 kHz. Use -ar/--sample-rate to specify a different output sample rate.
367
+ Dynamic mode will automatically change the sample rate to 192 kHz. Use `-ar`/`--sample-rate` to specify a different output sample rate.
312
368
 
313
369
  ### Audio Encoding
314
370
 
@@ -444,7 +500,11 @@ For most cases, linear mode is recommended. Dynamic mode should only be used whe
444
500
 
445
501
  * When the required gain adjustment to meet the integrated loudness target would result in the true peak exceeding the specified true peak limit. This is because linear processing alone cannot reduce peaks without affecting the entire signal. For example, if a file needs to be amplified by 6 dB to reach the target integrated loudness, but doing so would push the true peak above the specified limit, the filter might switch to dynamic mode to handle this situation. If your content allows for it, you can increase the true peak target to give more headroom for linear processing. If you're consistently running into true peak issues, you might also consider lowering your target integrated loudness level.
446
502
 
447
- At this time, the `loudnorm` filter in ffmpeg does not provide a way to force linear mode when the input loudness range exceeds the target or when the true peak would be exceeded. The `--keep-loudness-range-target` option can be used to keep the input loudness range target above the specified target, but it will not force linear mode in all cases. We are working on a solution to handle this automatically!
503
+ At this time, the `loudnorm` filter in ffmpeg does not provide a way to force linear mode when the input loudness range exceeds the target or when the true peak would be exceeded. There are some options to mitigate this:
504
+
505
+ - The `--keep-lra-above-loudness-range-target` option can be used to keep the input loudness range above the specified target, but it will not force linear mode in all cases.
506
+ - Similarly, the `--keep-loudness-range-target` option can be used to keep the input loudness range target.
507
+ - The `--lower-only` option can be used to skip the normalization pass completely if the measured loudness is lower than the target loudness.
448
508
 
449
509
  ### The program doesn't work because the "loudnorm" filter can't be found
450
510
 
@@ -611,6 +671,30 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
611
671
  # Changelog
612
672
 
613
673
 
674
+ ## v1.31.1 (2025-02-19)
675
+
676
+ * Fix bitrate setting for libvorbis, fixes #277.
677
+
678
+ * Update issue templates.
679
+
680
+ * Prevent blank issues.
681
+
682
+
683
+ ## v1.31.0 (2024-12-15)
684
+
685
+ * Update docs and completions.
686
+
687
+ * Implement `--auto-lower-loudness-target`
688
+
689
+ * Fix deprecations and mypy --strict errors.
690
+
691
+ * Feat: add completions.
692
+
693
+ * Docs: update explainer.
694
+
695
+ * Docs: update docs to include lower-only.
696
+
697
+
614
698
  ## v1.30.0 (2024-11-22)
615
699
 
616
700
  * Change lower-only message to warning.
@@ -0,0 +1,16 @@
1
+ ffmpeg_normalize/__init__.py,sha256=aAhlk93ZE6SQcWUDzZQcw9vJh0bJcKEUNFGhVc5ZIto,453
2
+ ffmpeg_normalize/__main__.py,sha256=AuyIcMU1WxGamczZtZyISfgMsEIt3Y1kSJ87dCoH9J4,19393
3
+ ffmpeg_normalize/_cmd_utils.py,sha256=S7PLXQAZHmJ30RM9K6b--vXuxMf-cQHtaFOPtILxz-4,5360
4
+ ffmpeg_normalize/_errors.py,sha256=brTQ4osJ4fTA8wnyMPVVYfGwJ0wqeShRFydTEwi_VEY,48
5
+ ffmpeg_normalize/_ffmpeg_normalize.py,sha256=VoxhER57Ew0RKC_WcGpoCmyKKoqsHYuqNFMm7z4BPM4,11080
6
+ ffmpeg_normalize/_logger.py,sha256=3Ap4Fxg7xGrzz7h4IGuNEf0KKstx0Rq_eLbHPrHzcrI,1841
7
+ ffmpeg_normalize/_media_file.py,sha256=BnFTOGOAxzofGS5k7ueG3k6LsUxro_VM6qWCQH5o81A,18258
8
+ ffmpeg_normalize/_streams.py,sha256=LIllXl4SKLxlyPVjD3ieHqc_byF2eUTjnK-clh2g_CY,20211
9
+ ffmpeg_normalize/_version.py,sha256=VFBXpgylen4bFLDe4KfNX1OmaXu_M6UnJYtTvQRQz4c,23
10
+ ffmpeg_normalize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ ffmpeg_normalize-1.31.1.dist-info/LICENSE,sha256=zeqAHGWrSIwdPHsZMZv1_N0gGFO1xxjcZEz9CplR4EM,1086
12
+ ffmpeg_normalize-1.31.1.dist-info/METADATA,sha256=Trwdo0wpKwL38BgLgBLXzAWhqyMjhEnKDgL2jFHtMXU,60209
13
+ ffmpeg_normalize-1.31.1.dist-info/WHEEL,sha256=fS9sRbCBHs7VFcwJLnLXN1MZRR0_TVTxvXKzOnaSFs8,110
14
+ ffmpeg_normalize-1.31.1.dist-info/entry_points.txt,sha256=X0EC5ptb0iGOxrk3Aa65dVQtvUixngLd_2-iAtSixdc,68
15
+ ffmpeg_normalize-1.31.1.dist-info/top_level.txt,sha256=wnUkr17ckPrrU1JsxZQiXbEBUnHKsC64yck-MemEBuI,17
16
+ ffmpeg_normalize-1.31.1.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- ffmpeg_normalize/__init__.py,sha256=aAhlk93ZE6SQcWUDzZQcw9vJh0bJcKEUNFGhVc5ZIto,453
2
- ffmpeg_normalize/__main__.py,sha256=mixrjY4XggMMpy31Q23MtUj85LB4EUQxlE-_mpVgrbk,19035
3
- ffmpeg_normalize/_cmd_utils.py,sha256=A6quxjOl5tLHxgcs7SdMXzuOCP_JjUTzL1iUD4G7DJk,5361
4
- ffmpeg_normalize/_errors.py,sha256=brTQ4osJ4fTA8wnyMPVVYfGwJ0wqeShRFydTEwi_VEY,48
5
- ffmpeg_normalize/_ffmpeg_normalize.py,sha256=9AkUoUi-qLH8fPjw4aZcKA3NDCpfBjfPtGhebpZFgZs,10856
6
- ffmpeg_normalize/_logger.py,sha256=3Ap4Fxg7xGrzz7h4IGuNEf0KKstx0Rq_eLbHPrHzcrI,1841
7
- ffmpeg_normalize/_media_file.py,sha256=tOtoVc931ynCI-BkbBWw2sMsMy9ZdactiwRZGFTTazs,17969
8
- ffmpeg_normalize/_streams.py,sha256=dAwhlSirx4OlW_IoWcOSedB2BCRnIAAeWMbjWQi-QP8,19605
9
- ffmpeg_normalize/_version.py,sha256=a214_SpyTblEsutXQ0q4k-AFdSRCCSNLN1gRUBDn6x4,23
10
- ffmpeg_normalize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- ffmpeg_normalize-1.30.0.dist-info/LICENSE,sha256=zeqAHGWrSIwdPHsZMZv1_N0gGFO1xxjcZEz9CplR4EM,1086
12
- ffmpeg_normalize-1.30.0.dist-info/METADATA,sha256=Zem-Rn-UkzYuhKGR6uanCFDG_Doea_DzKpCIGa5cJqw,57487
13
- ffmpeg_normalize-1.30.0.dist-info/WHEEL,sha256=fS9sRbCBHs7VFcwJLnLXN1MZRR0_TVTxvXKzOnaSFs8,110
14
- ffmpeg_normalize-1.30.0.dist-info/entry_points.txt,sha256=X0EC5ptb0iGOxrk3Aa65dVQtvUixngLd_2-iAtSixdc,68
15
- ffmpeg_normalize-1.30.0.dist-info/top_level.txt,sha256=wnUkr17ckPrrU1JsxZQiXbEBUnHKsC64yck-MemEBuI,17
16
- ffmpeg_normalize-1.30.0.dist-info/RECORD,,