ffmpeg-normalize 1.29.2__tar.gz → 1.31.0__tar.gz

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.
Files changed (28) hide show
  1. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/CHANGELOG.md +38 -0
  2. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/PKG-INFO +122 -12
  3. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/README.md +80 -8
  4. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/__main__.py +31 -3
  5. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/_cmd_utils.py +3 -3
  6. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/_ffmpeg_normalize.py +11 -2
  7. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/_media_file.py +49 -20
  8. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/_streams.py +79 -92
  9. ffmpeg_normalize-1.31.0/ffmpeg_normalize/_version.py +1 -0
  10. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize.egg-info/PKG-INFO +121 -11
  11. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/setup.py +3 -3
  12. ffmpeg-normalize-1.29.2/ffmpeg_normalize/_version.py +0 -1
  13. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/LICENSE +0 -0
  14. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/__init__.py +0 -0
  15. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/_errors.py +0 -0
  16. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/_logger.py +0 -0
  17. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize/py.typed +0 -0
  18. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize.egg-info/SOURCES.txt +0 -0
  19. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize.egg-info/dependency_links.txt +0 -0
  20. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize.egg-info/entry_points.txt +0 -0
  21. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize.egg-info/not-zip-safe +0 -0
  22. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize.egg-info/requires.txt +0 -0
  23. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/ffmpeg_normalize.egg-info/top_level.txt +0 -0
  24. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/setup.cfg +0 -0
  25. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/test/out.mp4 +0 -0
  26. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/test/test.mp4 +0 -0
  27. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/test/test.py +0 -0
  28. {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.31.0}/test/test.wav +0 -0
@@ -1,6 +1,44 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## v1.31.0 (2024-12-15)
5
+
6
+ * Update docs and completions.
7
+
8
+ * Implement `--auto-lower-loudness-target`
9
+
10
+ * Fix deprecations and mypy --strict errors.
11
+
12
+ * Feat: add completions.
13
+
14
+ * Docs: update explainer.
15
+
16
+ * Docs: update docs to include lower-only.
17
+
18
+
19
+ ## v1.30.0 (2024-11-22)
20
+
21
+ * Change lower-only message to warning.
22
+
23
+ * Make setup name PEP 625 compliant.
24
+
25
+ * Docs: add @ahmetsait as a contributor.
26
+
27
+ * Implement `--lower-only`
28
+
29
+ * Fix: `--print-stats` only outputs the last stream.
30
+
31
+ * More robust `loudnorm` output parsing.
32
+
33
+ * Remove unnecessary conversions.
34
+
35
+ * Update .editorconfig.
36
+
37
+ * Remove python 3.8, add python 3.12, 3.13.
38
+
39
+ * Add README on file size.
40
+
41
+
4
42
  ## v1.29.2 (2024-11-18)
5
43
 
6
44
  * Fix: show percentage with two decimal digits in progress.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: ffmpeg-normalize
3
- Version: 1.29.2
2
+ Name: ffmpeg_normalize
3
+ Version: 1.31.0
4
4
  Summary: Normalize audio via ffmpeg
5
5
  Home-page: https://github.com/slhck/ffmpeg-normalize
6
6
  Author: Werner Robitza
@@ -15,12 +15,12 @@ Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Natural Language :: English
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
18
  Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
- Requires-Python: >=3.8
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Requires-Python: >=3.9
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
 
@@ -31,7 +31,7 @@ License-File: LICENSE
31
31
  ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/slhck/ffmpeg-normalize/python-package.yml)
32
32
 
33
33
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
34
- [![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors-)
34
+ [![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat-square)](#contributors-)
35
35
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
36
36
 
37
37
  A utility for batch-normalizing audio using ffmpeg.
@@ -54,6 +54,7 @@ Read on for more info.
54
54
  - [Requirements](#requirements)
55
55
  - [ffmpeg](#ffmpeg)
56
56
  - [Installation](#installation)
57
+ - [Shell Completions](#shell-completions)
57
58
  - [Usage with Docker](#usage-with-docker)
58
59
  - [High LeveL Introduction](#high-level-introduction)
59
60
  - [Basic Usage](#basic-usage)
@@ -69,6 +70,7 @@ Read on for more info.
69
70
  - [Environment Variables](#environment-variables)
70
71
  - [API](#api)
71
72
  - [FAQ](#faq)
73
+ - [My output file is too large?](#my-output-file-is-too-large)
72
74
  - [What options should I choose for the EBU R128 filter? What is linear and dynamic mode?](#what-options-should-i-choose-for-the-ebu-r128-filter-what-is-linear-and-dynamic-mode)
73
75
  - [The program doesn't work because the "loudnorm" filter can't be found](#the-program-doesnt-work-because-the-loudnorm-filter-cant-be-found)
74
76
  - [Should I use this to normalize my music collection?](#should-i-use-this-to-normalize-my-music-collection)
@@ -88,7 +90,7 @@ Read on for more info.
88
90
 
89
91
  ## Requirements
90
92
 
91
- You need Python 3.8 or higher, and ffmpeg.
93
+ You need Python 3.9 or higher, and ffmpeg.
92
94
 
93
95
  ### ffmpeg
94
96
 
@@ -129,6 +131,54 @@ Or download this repository, then run `pip3 install .`.
129
131
 
130
132
  To later upgrade to the latest version, run `pip3 install --upgrade ffmpeg-normalize`.
131
133
 
134
+ ### Shell Completions
135
+
136
+ This tool provides shell completions for bash and zsh. To install them:
137
+
138
+ <!--
139
+ Note to self: Generate the shtab ones with:
140
+
141
+ shtab --shell=bash -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.bash
142
+ shtab --shell=zsh -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.zsh
143
+
144
+ but these are not properly working yet.
145
+ -->
146
+
147
+ #### Bash
148
+
149
+ 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.
150
+
151
+ ```bash
152
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
153
+ -o /usr/local/etc/bash_completion.d/ffmpeg-normalize
154
+ ```
155
+
156
+ Without bash-completion, you can manually install the completion script:
157
+
158
+ ```bash
159
+ # create completions directory if it doesn't exist
160
+ mkdir -p ~/.bash_completions.d
161
+
162
+ # download and install completion script
163
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
164
+ -o ~/.bash_completions.d/ffmpeg-normalize
165
+
166
+ # source it in your ~/.bashrc
167
+ echo 'source ~/.bash_completions.d/ffmpeg-normalize' >> ~/.bashrc
168
+ ```
169
+
170
+ #### Zsh
171
+
172
+ Download the completion script and place it in the default `site-functions` directory:
173
+
174
+ ```bash
175
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize.zsh \
176
+ -o /usr/local/share/zsh/site-functions/
177
+ ```
178
+
179
+ You may choose any other directory that is in your `$FPATH` variable.
180
+ Make sure your `.zshrc` file contains `autoload -Uz compinit && compinit`.
181
+
132
182
  ## Usage with Docker
133
183
 
134
184
  You can use the pre-built image from Docker Hub:
@@ -280,10 +330,7 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
280
330
 
281
331
  - `--keep-lra-above-loudness-range-target`: Keep input loudness range above loudness range target.
282
332
 
283
- - `LOUDNESS_RANGE_TARGET` for input loudness range `<= LOUDNESS_RANGE_TARGET` or
284
- - keep input loudness range target above `LOUDNESS_RANGE_TARGET`.
285
-
286
- as alternative to `--keep-loudness-range-target` to allow for linear normalization.
333
+ Can be used as an alternative to `--keep-loudness-range-target` to allow for linear normalization.
287
334
 
288
335
  - `-tp TRUE_PEAK, --true-peak TRUE_PEAK`: EBU Maximum True Peak in dBTP (default: -2.0).
289
336
 
@@ -295,6 +342,16 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
295
342
 
296
343
  Range is -99.0 - +99.0.
297
344
 
345
+ - `--lower-only`: Whether the audio should not increase in loudness.
346
+
347
+ 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.
348
+
349
+ - `--auto-lower-loudness-target`: Automatically lower EBU Integrated Loudness Target.
350
+
351
+ Automatically lower EBU Integrated Loudness Target to prevent falling back to dynamic filtering.
352
+
353
+ Makes sure target loudness is lower than measured loudness minus peak loudness (input_i - input_tp) by a small amount.
354
+
298
355
  - `--dual-mono`: Treat mono input files as "dual-mono".
299
356
 
300
357
  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.
@@ -303,7 +360,7 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
303
360
 
304
361
  Instead of applying linear EBU R128 normalization, choose a dynamic normalization. This is not usually recommended.
305
362
 
306
- Dynamic mode will automatically change the sample rate to 192 kHz. Use -ar/--sample-rate to specify a different output sample rate.
363
+ Dynamic mode will automatically change the sample rate to 192 kHz. Use `-ar`/`--sample-rate` to specify a different output sample rate.
307
364
 
308
365
  ### Audio Encoding
309
366
 
@@ -405,6 +462,16 @@ For more information see the [API documentation](https://htmlpreview.github.io/?
405
462
 
406
463
  ## FAQ
407
464
 
465
+ ### My output file is too large?
466
+
467
+ This is because the default output codec is PCM, which is uncompressed. If you want to reduce the file size, you can specify an audio codec with `-c:a` (e.g., `-c:a aac` for ffmpeg's built-in AAC encoder), and optionally a bitrate with `-b:a`.
468
+
469
+ For example:
470
+
471
+ ```bash
472
+ ffmpeg-normalize input.wav -o output.m4a -c:a aac -b:a 192k
473
+ ```
474
+
408
475
  ### What options should I choose for the EBU R128 filter? What is linear and dynamic mode?
409
476
 
410
477
  EBU R128 is a method for normalizing audio loudness across different tracks or programs. It works by analyzing the audio content and adjusting it to meet specific loudness targets. The main components are:
@@ -429,7 +496,11 @@ For most cases, linear mode is recommended. Dynamic mode should only be used whe
429
496
 
430
497
  * 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.
431
498
 
432
- 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!
499
+ 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:
500
+
501
+ - 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.
502
+ - Similarly, the `--keep-loudness-range-target` option can be used to keep the input loudness range target.
503
+ - The `--lower-only` option can be used to skip the normalization pass completely if the measured loudness is lower than the target loudness.
433
504
 
434
505
  ### The program doesn't work because the "loudnorm" filter can't be found
435
506
 
@@ -548,6 +619,7 @@ If you found this program useful and feel like giving back, feel free to send a
548
619
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/psavva"><img src="https://avatars.githubusercontent.com/u/1454758?v=4?s=100" width="100px;" alt="Panayiotis Savva"/><br /><sub><b>Panayiotis Savva</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=psavva" title="Code">💻</a></td>
549
620
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/HighMans"><img src="https://avatars.githubusercontent.com/u/42877729?v=4?s=100" width="100px;" alt="HighMans"/><br /><sub><b>HighMans</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=HighMans" title="Code">💻</a></td>
550
621
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/kanjieater"><img src="https://avatars.githubusercontent.com/u/32607317?v=4?s=100" width="100px;" alt="kanjieater"/><br /><sub><b>kanjieater</b></sub></a><br /><a href="#ideas-kanjieater" title="Ideas, Planning, & Feedback">🤔</a></td>
622
+ <td align="center" valign="top" width="14.28%"><a href="https://ahmetsait.com/"><img src="https://avatars.githubusercontent.com/u/8372246?v=4?s=100" width="100px;" alt="Ahmet Sait"/><br /><sub><b>Ahmet Sait</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=ahmetsait" title="Code">💻</a></td>
551
623
  </tr>
552
624
  </tbody>
553
625
  <tfoot>
@@ -595,6 +667,44 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
595
667
  # Changelog
596
668
 
597
669
 
670
+ ## v1.31.0 (2024-12-15)
671
+
672
+ * Update docs and completions.
673
+
674
+ * Implement `--auto-lower-loudness-target`
675
+
676
+ * Fix deprecations and mypy --strict errors.
677
+
678
+ * Feat: add completions.
679
+
680
+ * Docs: update explainer.
681
+
682
+ * Docs: update docs to include lower-only.
683
+
684
+
685
+ ## v1.30.0 (2024-11-22)
686
+
687
+ * Change lower-only message to warning.
688
+
689
+ * Make setup name PEP 625 compliant.
690
+
691
+ * Docs: add @ahmetsait as a contributor.
692
+
693
+ * Implement `--lower-only`
694
+
695
+ * Fix: `--print-stats` only outputs the last stream.
696
+
697
+ * More robust `loudnorm` output parsing.
698
+
699
+ * Remove unnecessary conversions.
700
+
701
+ * Update .editorconfig.
702
+
703
+ * Remove python 3.8, add python 3.12, 3.13.
704
+
705
+ * Add README on file size.
706
+
707
+
598
708
  ## v1.29.2 (2024-11-18)
599
709
 
600
710
  * Fix: show percentage with two decimal digits in progress.
@@ -5,7 +5,7 @@
5
5
  ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/slhck/ffmpeg-normalize/python-package.yml)
6
6
 
7
7
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
8
- [![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors-)
8
+ [![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat-square)](#contributors-)
9
9
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
10
10
 
11
11
  A utility for batch-normalizing audio using ffmpeg.
@@ -28,6 +28,7 @@ Read on for more info.
28
28
  - [Requirements](#requirements)
29
29
  - [ffmpeg](#ffmpeg)
30
30
  - [Installation](#installation)
31
+ - [Shell Completions](#shell-completions)
31
32
  - [Usage with Docker](#usage-with-docker)
32
33
  - [High LeveL Introduction](#high-level-introduction)
33
34
  - [Basic Usage](#basic-usage)
@@ -43,6 +44,7 @@ Read on for more info.
43
44
  - [Environment Variables](#environment-variables)
44
45
  - [API](#api)
45
46
  - [FAQ](#faq)
47
+ - [My output file is too large?](#my-output-file-is-too-large)
46
48
  - [What options should I choose for the EBU R128 filter? What is linear and dynamic mode?](#what-options-should-i-choose-for-the-ebu-r128-filter-what-is-linear-and-dynamic-mode)
47
49
  - [The program doesn't work because the "loudnorm" filter can't be found](#the-program-doesnt-work-because-the-loudnorm-filter-cant-be-found)
48
50
  - [Should I use this to normalize my music collection?](#should-i-use-this-to-normalize-my-music-collection)
@@ -62,7 +64,7 @@ Read on for more info.
62
64
 
63
65
  ## Requirements
64
66
 
65
- You need Python 3.8 or higher, and ffmpeg.
67
+ You need Python 3.9 or higher, and ffmpeg.
66
68
 
67
69
  ### ffmpeg
68
70
 
@@ -103,6 +105,54 @@ Or download this repository, then run `pip3 install .`.
103
105
 
104
106
  To later upgrade to the latest version, run `pip3 install --upgrade ffmpeg-normalize`.
105
107
 
108
+ ### Shell Completions
109
+
110
+ This tool provides shell completions for bash and zsh. To install them:
111
+
112
+ <!--
113
+ Note to self: Generate the shtab ones with:
114
+
115
+ shtab --shell=bash -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.bash
116
+ shtab --shell=zsh -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.zsh
117
+
118
+ but these are not properly working yet.
119
+ -->
120
+
121
+ #### Bash
122
+
123
+ 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.
124
+
125
+ ```bash
126
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
127
+ -o /usr/local/etc/bash_completion.d/ffmpeg-normalize
128
+ ```
129
+
130
+ Without bash-completion, you can manually install the completion script:
131
+
132
+ ```bash
133
+ # create completions directory if it doesn't exist
134
+ mkdir -p ~/.bash_completions.d
135
+
136
+ # download and install completion script
137
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
138
+ -o ~/.bash_completions.d/ffmpeg-normalize
139
+
140
+ # source it in your ~/.bashrc
141
+ echo 'source ~/.bash_completions.d/ffmpeg-normalize' >> ~/.bashrc
142
+ ```
143
+
144
+ #### Zsh
145
+
146
+ Download the completion script and place it in the default `site-functions` directory:
147
+
148
+ ```bash
149
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize.zsh \
150
+ -o /usr/local/share/zsh/site-functions/
151
+ ```
152
+
153
+ You may choose any other directory that is in your `$FPATH` variable.
154
+ Make sure your `.zshrc` file contains `autoload -Uz compinit && compinit`.
155
+
106
156
  ## Usage with Docker
107
157
 
108
158
  You can use the pre-built image from Docker Hub:
@@ -254,10 +304,7 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
254
304
 
255
305
  - `--keep-lra-above-loudness-range-target`: Keep input loudness range above loudness range target.
256
306
 
257
- - `LOUDNESS_RANGE_TARGET` for input loudness range `<= LOUDNESS_RANGE_TARGET` or
258
- - keep input loudness range target above `LOUDNESS_RANGE_TARGET`.
259
-
260
- as alternative to `--keep-loudness-range-target` to allow for linear normalization.
307
+ Can be used as an alternative to `--keep-loudness-range-target` to allow for linear normalization.
261
308
 
262
309
  - `-tp TRUE_PEAK, --true-peak TRUE_PEAK`: EBU Maximum True Peak in dBTP (default: -2.0).
263
310
 
@@ -269,6 +316,16 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
269
316
 
270
317
  Range is -99.0 - +99.0.
271
318
 
319
+ - `--lower-only`: Whether the audio should not increase in loudness.
320
+
321
+ 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.
322
+
323
+ - `--auto-lower-loudness-target`: Automatically lower EBU Integrated Loudness Target.
324
+
325
+ Automatically lower EBU Integrated Loudness Target to prevent falling back to dynamic filtering.
326
+
327
+ Makes sure target loudness is lower than measured loudness minus peak loudness (input_i - input_tp) by a small amount.
328
+
272
329
  - `--dual-mono`: Treat mono input files as "dual-mono".
273
330
 
274
331
  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.
@@ -277,7 +334,7 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
277
334
 
278
335
  Instead of applying linear EBU R128 normalization, choose a dynamic normalization. This is not usually recommended.
279
336
 
280
- Dynamic mode will automatically change the sample rate to 192 kHz. Use -ar/--sample-rate to specify a different output sample rate.
337
+ Dynamic mode will automatically change the sample rate to 192 kHz. Use `-ar`/`--sample-rate` to specify a different output sample rate.
281
338
 
282
339
  ### Audio Encoding
283
340
 
@@ -379,6 +436,16 @@ For more information see the [API documentation](https://htmlpreview.github.io/?
379
436
 
380
437
  ## FAQ
381
438
 
439
+ ### My output file is too large?
440
+
441
+ This is because the default output codec is PCM, which is uncompressed. If you want to reduce the file size, you can specify an audio codec with `-c:a` (e.g., `-c:a aac` for ffmpeg's built-in AAC encoder), and optionally a bitrate with `-b:a`.
442
+
443
+ For example:
444
+
445
+ ```bash
446
+ ffmpeg-normalize input.wav -o output.m4a -c:a aac -b:a 192k
447
+ ```
448
+
382
449
  ### What options should I choose for the EBU R128 filter? What is linear and dynamic mode?
383
450
 
384
451
  EBU R128 is a method for normalizing audio loudness across different tracks or programs. It works by analyzing the audio content and adjusting it to meet specific loudness targets. The main components are:
@@ -403,7 +470,11 @@ For most cases, linear mode is recommended. Dynamic mode should only be used whe
403
470
 
404
471
  * 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.
405
472
 
406
- 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!
473
+ 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:
474
+
475
+ - 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.
476
+ - Similarly, the `--keep-loudness-range-target` option can be used to keep the input loudness range target.
477
+ - The `--lower-only` option can be used to skip the normalization pass completely if the measured loudness is lower than the target loudness.
407
478
 
408
479
  ### The program doesn't work because the "loudnorm" filter can't be found
409
480
 
@@ -522,6 +593,7 @@ If you found this program useful and feel like giving back, feel free to send a
522
593
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/psavva"><img src="https://avatars.githubusercontent.com/u/1454758?v=4?s=100" width="100px;" alt="Panayiotis Savva"/><br /><sub><b>Panayiotis Savva</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=psavva" title="Code">💻</a></td>
523
594
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/HighMans"><img src="https://avatars.githubusercontent.com/u/42877729?v=4?s=100" width="100px;" alt="HighMans"/><br /><sub><b>HighMans</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=HighMans" title="Code">💻</a></td>
524
595
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/kanjieater"><img src="https://avatars.githubusercontent.com/u/32607317?v=4?s=100" width="100px;" alt="kanjieater"/><br /><sub><b>kanjieater</b></sub></a><br /><a href="#ideas-kanjieater" title="Ideas, Planning, & Feedback">🤔</a></td>
596
+ <td align="center" valign="top" width="14.28%"><a href="https://ahmetsait.com/"><img src="https://avatars.githubusercontent.com/u/8372246?v=4?s=100" width="100px;" alt="Ahmet Sait"/><br /><sub><b>Ahmet Sait</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=ahmetsait" title="Code">💻</a></td>
525
597
  </tr>
526
598
  </tbody>
527
599
  <tfoot>
@@ -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
  )
@@ -235,6 +233,34 @@ def create_parser() -> argparse.ArgumentParser:
235
233
  default=0.0,
236
234
  )
237
235
 
236
+ group_ebu.add_argument(
237
+ "--lower-only",
238
+ action="store_true",
239
+ help=textwrap.dedent(
240
+ """\
241
+ Whether the audio should not increase in loudness.
242
+
243
+ If the measured loudness from the first pass is lower than the target
244
+ loudness then normalization pass will be skipped for the measured audio
245
+ source.
246
+ """
247
+ ),
248
+ )
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
+
238
264
  group_ebu.add_argument(
239
265
  "--dual-mono",
240
266
  action="store_true",
@@ -514,6 +540,8 @@ def main() -> None:
514
540
  keep_lra_above_loudness_range_target=cli_args.keep_lra_above_loudness_range_target,
515
541
  true_peak=cli_args.true_peak,
516
542
  offset=cli_args.offset,
543
+ lower_only=cli_args.lower_only,
544
+ auto_lower_loudness_target=cli_args.auto_lower_loudness_target,
517
545
  dual_mono=cli_args.dual_mono,
518
546
  dynamic=cli_args.dynamic,
519
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
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import json
4
4
  import logging
5
5
  import os
6
+ import sys
7
+ from itertools import chain
6
8
  from typing import TYPE_CHECKING, Literal
7
9
 
8
10
  from tqdm import tqdm
@@ -58,6 +60,8 @@ class FFmpegNormalize:
58
60
  keep_lra_above_loudness_range_target (bool, optional): Keep input loudness range above loudness range target. Defaults to False.
59
61
  true_peak (float, optional): True peak. Defaults to -2.0.
60
62
  offset (float, optional): Offset. Defaults to 0.0.
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.
61
65
  dual_mono (bool, optional): Dual mono. Defaults to False.
62
66
  dynamic (bool, optional): Dynamic. Defaults to False.
63
67
  audio_codec (str, optional): Audio codec. Defaults to "pcm_s16le".
@@ -94,6 +98,8 @@ class FFmpegNormalize:
94
98
  keep_lra_above_loudness_range_target: bool = False,
95
99
  true_peak: float = -2.0,
96
100
  offset: float = 0.0,
101
+ lower_only: bool = False,
102
+ auto_lower_loudness_target: bool = False,
97
103
  dual_mono: bool = False,
98
104
  dynamic: bool = False,
99
105
  audio_codec: str = "pcm_s16le",
@@ -164,6 +170,8 @@ class FFmpegNormalize:
164
170
 
165
171
  self.true_peak = check_range(true_peak, -9, 0, name="true_peak")
166
172
  self.offset = check_range(offset, -99, 99, name="offset")
173
+ self.lower_only = lower_only
174
+ self.auto_lower_loudness_target = auto_lower_loudness_target
167
175
 
168
176
  # Ensure library user is passing correct types
169
177
  assert isinstance(dual_mono, bool), "dual_mono must be bool"
@@ -254,5 +262,6 @@ class FFmpegNormalize:
254
262
 
255
263
  _logger.info(f"Normalized file written to {media_file.output_file}")
256
264
 
257
- if self.print_stats and self.stats:
258
- print(json.dumps(self.stats, indent=4))
265
+ if self.print_stats:
266
+ json.dump(list(chain.from_iterable(media_file.get_stats() for media_file in self.media_files)), sys.stdout, indent=4)
267
+ print()
@@ -6,13 +6,18 @@ import re
6
6
  import shlex
7
7
  from shutil import move, rmtree
8
8
  from tempfile import mkdtemp
9
- from typing import TYPE_CHECKING, Iterator, Literal, TypedDict
9
+ from typing import TYPE_CHECKING, Iterable, Iterator, Literal, TypedDict
10
10
 
11
11
  from tqdm import tqdm
12
12
 
13
13
  from ._cmd_utils import DUR_REGEX, NUL, CommandRunner
14
14
  from ._errors import FFmpegNormalizeError
15
- from ._streams import AudioStream, SubtitleStream, VideoStream
15
+ from ._streams import (
16
+ AudioStream,
17
+ LoudnessStatisticsWithMetadata,
18
+ SubtitleStream,
19
+ VideoStream,
20
+ )
16
21
 
17
22
  if TYPE_CHECKING:
18
23
  from ffmpeg_normalize import FFmpegNormalize
@@ -240,11 +245,6 @@ class MediaFile:
240
245
  for _ in fun():
241
246
  pass
242
247
 
243
- # set initial stats (for dry-runs, this is the only thing we need to do)
244
- self.ffmpeg_normalize.stats = [
245
- audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
246
- ]
247
-
248
248
  def _get_audio_filter_cmd(self) -> tuple[str, list[str]]:
249
249
  """
250
250
  Return the audio filter command and output labels needed.
@@ -256,10 +256,40 @@ class MediaFile:
256
256
  output_labels = []
257
257
 
258
258
  for audio_stream in self.streams["audio"].values():
259
- if self.ffmpeg_normalize.normalization_type == "ebu":
260
- normalization_filter = audio_stream.get_second_pass_opts_ebu()
259
+ skip_normalization = False
260
+ if self.ffmpeg_normalize.lower_only:
261
+ if self.ffmpeg_normalize.normalization_type == "ebu":
262
+ if (
263
+ audio_stream.loudness_statistics["ebu_pass1"] is not None
264
+ and audio_stream.loudness_statistics["ebu_pass1"]["input_i"]
265
+ < self.ffmpeg_normalize.target_level
266
+ ):
267
+ skip_normalization = True
268
+ elif self.ffmpeg_normalize.normalization_type == "peak":
269
+ if (
270
+ audio_stream.loudness_statistics["max"] is not None
271
+ and audio_stream.loudness_statistics["max"]
272
+ < self.ffmpeg_normalize.target_level
273
+ ):
274
+ skip_normalization = True
275
+ elif self.ffmpeg_normalize.normalization_type == "rms":
276
+ if (
277
+ audio_stream.loudness_statistics["mean"] is not None
278
+ and audio_stream.loudness_statistics["mean"]
279
+ < self.ffmpeg_normalize.target_level
280
+ ):
281
+ skip_normalization = True
282
+
283
+ if skip_normalization:
284
+ _logger.warning(
285
+ f"Stream {audio_stream.stream_id} had measured input loudness lower than target, skipping normalization."
286
+ )
287
+ normalization_filter = "acopy"
261
288
  else:
262
- normalization_filter = audio_stream.get_second_pass_opts_peakrms()
289
+ if self.ffmpeg_normalize.normalization_type == "ebu":
290
+ normalization_filter = audio_stream.get_second_pass_opts_ebu()
291
+ else:
292
+ normalization_filter = audio_stream.get_second_pass_opts_peakrms()
263
293
 
264
294
  input_label = f"[0:{audio_stream.stream_id}]"
265
295
  output_label = f"[norm{audio_stream.stream_id}]"
@@ -421,16 +451,10 @@ class MediaFile:
421
451
  # in the second pass, we do not normalize stream-by-stream, so we set the stats based on the
422
452
  # overall output (which includes multiple loudnorm stats)
423
453
  if self.ffmpeg_normalize.normalization_type == "ebu":
424
- all_stats = AudioStream.prune_and_parse_loudnorm_output(
425
- output, num_stats=len(self.streams["audio"])
426
- )
427
- for idx, audio_stream in enumerate(self.streams["audio"].values()):
428
- audio_stream.set_second_pass_stats(all_stats[idx])
429
-
430
- # collect all stats for the final report, again (overwrite the input)
431
- self.ffmpeg_normalize.stats = [
432
- audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
433
- ]
454
+ all_stats = AudioStream.prune_and_parse_loudnorm_output(output)
455
+ for stream_id, audio_stream in self.streams["audio"].items():
456
+ if stream_id in all_stats:
457
+ audio_stream.set_second_pass_stats(all_stats[stream_id])
434
458
 
435
459
  # warn if self.media_file.ffmpeg_normalize.dynamic == False and any of the second pass stats contain "normalization_type" == "dynamic"
436
460
  if self.ffmpeg_normalize.dynamic is False:
@@ -446,3 +470,8 @@ class MediaFile:
446
470
  )
447
471
 
448
472
  _logger.debug("Normalization finished")
473
+
474
+ def get_stats(self) -> Iterable[LoudnessStatisticsWithMetadata]:
475
+ return (
476
+ audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
477
+ )
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
15
15
 
16
16
  _logger = logging.getLogger(__name__)
17
17
 
18
+ _loudnorm_pattern = re.compile(r"\[Parsed_loudnorm_(\d+)")
18
19
 
19
20
  class EbuLoudnessStatistics(TypedDict):
20
21
  input_i: float
@@ -166,7 +167,7 @@ class AudioStream(MediaStream):
166
167
  }
167
168
  return stats
168
169
 
169
- def set_second_pass_stats(self, stats: EbuLoudnessStatistics):
170
+ def set_second_pass_stats(self, stats: EbuLoudnessStatistics) -> None:
170
171
  """
171
172
  Set the EBU loudness statistics for the second pass.
172
173
 
@@ -320,58 +321,36 @@ class AudioStream(MediaStream):
320
321
  f"Loudnorm first pass command output: {CommandRunner.prune_ffmpeg_progress_from_output(output)}"
321
322
  )
322
323
 
323
- self.loudness_statistics["ebu_pass1"] = (
324
- AudioStream.prune_and_parse_loudnorm_output(
325
- output, num_stats=1
326
- )[0] # only one stream
327
- )
324
+ # only one stream
325
+ self.loudness_statistics["ebu_pass1"] = next(iter(AudioStream.prune_and_parse_loudnorm_output(output).values()))
328
326
 
329
327
  @staticmethod
330
328
  def prune_and_parse_loudnorm_output(
331
- output: str, num_stats: int = 1
332
- ) -> List[EbuLoudnessStatistics]:
329
+ output: str
330
+ ) -> dict[int, EbuLoudnessStatistics]:
333
331
  """
334
332
  Prune ffmpeg progress lines from output and parse the loudnorm filter output.
335
333
  There may be multiple outputs if multiple streams were processed.
336
334
 
337
335
  Args:
338
336
  output (str): The output from ffmpeg.
339
- num_stats (int): The number of loudnorm statistics to parse.
340
337
 
341
338
  Returns:
342
339
  list: The EBU loudness statistics.
343
340
  """
344
341
  pruned_output = CommandRunner.prune_ffmpeg_progress_from_output(output)
345
342
  output_lines = [line.strip() for line in pruned_output.split("\n")]
346
-
347
- ret = []
348
- idx = 0
349
- while True:
350
- _logger.debug(f"Parsing loudnorm stats for stream {idx}")
351
- loudnorm_stats = AudioStream._parse_loudnorm_output(
352
- output_lines, stream_index=idx
353
- )
354
- idx += 1
355
-
356
- if loudnorm_stats is None:
357
- continue
358
- ret.append(loudnorm_stats)
359
-
360
- if len(ret) >= num_stats:
361
- break
362
-
363
- return ret
343
+ return AudioStream._parse_loudnorm_output(output_lines)
364
344
 
365
345
  @staticmethod
366
346
  def _parse_loudnorm_output(
367
- output_lines: list[str], stream_index: Optional[int] = None
368
- ) -> Optional[EbuLoudnessStatistics]:
347
+ output_lines: list[str]
348
+ ) -> dict[int, EbuLoudnessStatistics]:
369
349
  """
370
350
  Parse the output of a loudnorm filter to get the EBU loudness statistics.
371
351
 
372
352
  Args:
373
353
  output_lines (list[str]): The output lines of the loudnorm filter.
374
- stream_index (int): The stream index, optional to filter out the correct stream. If unset, the first stream is used.
375
354
 
376
355
  Raises:
377
356
  FFmpegNormalizeError: When the output could not be parsed.
@@ -379,64 +358,58 @@ class AudioStream(MediaStream):
379
358
  Returns:
380
359
  EbuLoudnessStatistics: The EBU loudness statistics, if found.
381
360
  """
361
+ result = dict[int, EbuLoudnessStatistics]()
362
+ stream_index = -1
382
363
  loudnorm_start = 0
383
- loudnorm_end = 0
384
364
  for index, line in enumerate(output_lines):
385
- if line.startswith(f"[Parsed_loudnorm_{stream_index}"):
386
- loudnorm_start = index + 1
387
- continue
388
- if loudnorm_start and line.startswith("}"):
389
- loudnorm_end = index + 1
390
- break
391
-
392
- if not (loudnorm_start and loudnorm_end):
393
- if stream_index is not None:
394
- # not an error
395
- return None
396
-
397
- raise FFmpegNormalizeError(
398
- "Could not parse loudnorm stats; no loudnorm-related output found"
399
- )
400
-
401
- try:
402
- loudnorm_stats = json.loads(
403
- "\n".join(output_lines[loudnorm_start:loudnorm_end])
404
- )
405
-
406
- _logger.debug(
407
- f"Loudnorm stats for stream {stream_index} parsed: {json.dumps(loudnorm_stats)}"
408
- )
409
-
410
- for key in [
411
- "input_i",
412
- "input_tp",
413
- "input_lra",
414
- "input_thresh",
415
- "output_i",
416
- "output_tp",
417
- "output_lra",
418
- "output_thresh",
419
- "target_offset",
420
- "normalization_type",
421
- ]:
422
- if key not in loudnorm_stats:
423
- continue
424
- if key == "normalization_type":
425
- loudnorm_stats[key] = loudnorm_stats[key].lower()
426
- # handle infinite values
427
- elif float(loudnorm_stats[key]) == -float("inf"):
428
- loudnorm_stats[key] = -99
429
- elif float(loudnorm_stats[key]) == float("inf"):
430
- loudnorm_stats[key] = 0
431
- else:
432
- # convert to floats
433
- loudnorm_stats[key] = float(loudnorm_stats[key])
434
-
435
- return cast(EbuLoudnessStatistics, loudnorm_stats)
436
- except Exception as e:
437
- raise FFmpegNormalizeError(
438
- f"Could not parse loudnorm stats; wrong JSON format in string: {e}"
439
- )
365
+ if stream_index < 0:
366
+ if m := _loudnorm_pattern.match(line):
367
+ loudnorm_start = index + 1
368
+ stream_index = int(m.group(1))
369
+ else:
370
+ if line.startswith("}"):
371
+ loudnorm_end = index + 1
372
+ loudnorm_data = "\n".join(output_lines[loudnorm_start:loudnorm_end])
373
+
374
+ try:
375
+ loudnorm_stats = json.loads(loudnorm_data)
376
+
377
+ _logger.debug(
378
+ f"Loudnorm stats for stream {stream_index} parsed: {loudnorm_data}"
379
+ )
380
+
381
+ for key in [
382
+ "input_i",
383
+ "input_tp",
384
+ "input_lra",
385
+ "input_thresh",
386
+ "output_i",
387
+ "output_tp",
388
+ "output_lra",
389
+ "output_thresh",
390
+ "target_offset",
391
+ "normalization_type",
392
+ ]:
393
+ if key not in loudnorm_stats:
394
+ continue
395
+ if key == "normalization_type":
396
+ loudnorm_stats[key] = loudnorm_stats[key].lower()
397
+ # handle infinite values
398
+ elif float(loudnorm_stats[key]) == -float("inf"):
399
+ loudnorm_stats[key] = -99
400
+ elif float(loudnorm_stats[key]) == float("inf"):
401
+ loudnorm_stats[key] = 0
402
+ else:
403
+ # convert to floats
404
+ loudnorm_stats[key] = float(loudnorm_stats[key])
405
+
406
+ result[stream_index] = cast(EbuLoudnessStatistics, loudnorm_stats)
407
+ stream_index = -1
408
+ except Exception as e:
409
+ raise FFmpegNormalizeError(
410
+ f"Could not parse loudnorm stats; wrong JSON format in string: {e}"
411
+ )
412
+ return result
440
413
 
441
414
  def get_second_pass_opts_ebu(self) -> str:
442
415
  """
@@ -508,26 +481,40 @@ class AudioStream(MediaStream):
508
481
  "Specify -ar/--sample-rate to override it."
509
482
  )
510
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
+
511
498
  stats = self.loudness_statistics["ebu_pass1"]
512
499
 
513
500
  opts = {
514
- "i": self.media_file.ffmpeg_normalize.target_level,
501
+ "i": target_level,
515
502
  "lra": self.media_file.ffmpeg_normalize.loudness_range_target,
516
503
  "tp": self.media_file.ffmpeg_normalize.true_peak,
517
504
  "offset": self._constrain(
518
- float(stats["target_offset"]), -99, 99, name="target_offset"
505
+ stats["target_offset"], -99, 99, name="target_offset"
519
506
  ),
520
507
  "measured_i": self._constrain(
521
- float(stats["input_i"]), -99, 0, name="input_i"
508
+ stats["input_i"], -99, 0, name="input_i"
522
509
  ),
523
510
  "measured_lra": self._constrain(
524
- float(stats["input_lra"]), 0, 99, name="input_lra"
511
+ stats["input_lra"], 0, 99, name="input_lra"
525
512
  ),
526
513
  "measured_tp": self._constrain(
527
- float(stats["input_tp"]), -99, 99, name="input_tp"
514
+ stats["input_tp"], -99, 99, name="input_tp"
528
515
  ),
529
516
  "measured_thresh": self._constrain(
530
- float(stats["input_thresh"]), -99, 0, name="input_thresh"
517
+ stats["input_thresh"], -99, 0, name="input_thresh"
531
518
  ),
532
519
  "linear": "false" if self.media_file.ffmpeg_normalize.dynamic else "true",
533
520
  "print_format": "json",
@@ -0,0 +1 @@
1
+ __version__ = "1.31.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ffmpeg-normalize
3
- Version: 1.29.2
3
+ Version: 1.31.0
4
4
  Summary: Normalize audio via ffmpeg
5
5
  Home-page: https://github.com/slhck/ffmpeg-normalize
6
6
  Author: Werner Robitza
@@ -15,12 +15,12 @@ Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Natural Language :: English
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
18
  Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
- Requires-Python: >=3.8
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Requires-Python: >=3.9
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
 
@@ -31,7 +31,7 @@ License-File: LICENSE
31
31
  ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/slhck/ffmpeg-normalize/python-package.yml)
32
32
 
33
33
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
34
- [![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors-)
34
+ [![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat-square)](#contributors-)
35
35
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
36
36
 
37
37
  A utility for batch-normalizing audio using ffmpeg.
@@ -54,6 +54,7 @@ Read on for more info.
54
54
  - [Requirements](#requirements)
55
55
  - [ffmpeg](#ffmpeg)
56
56
  - [Installation](#installation)
57
+ - [Shell Completions](#shell-completions)
57
58
  - [Usage with Docker](#usage-with-docker)
58
59
  - [High LeveL Introduction](#high-level-introduction)
59
60
  - [Basic Usage](#basic-usage)
@@ -69,6 +70,7 @@ Read on for more info.
69
70
  - [Environment Variables](#environment-variables)
70
71
  - [API](#api)
71
72
  - [FAQ](#faq)
73
+ - [My output file is too large?](#my-output-file-is-too-large)
72
74
  - [What options should I choose for the EBU R128 filter? What is linear and dynamic mode?](#what-options-should-i-choose-for-the-ebu-r128-filter-what-is-linear-and-dynamic-mode)
73
75
  - [The program doesn't work because the "loudnorm" filter can't be found](#the-program-doesnt-work-because-the-loudnorm-filter-cant-be-found)
74
76
  - [Should I use this to normalize my music collection?](#should-i-use-this-to-normalize-my-music-collection)
@@ -88,7 +90,7 @@ Read on for more info.
88
90
 
89
91
  ## Requirements
90
92
 
91
- You need Python 3.8 or higher, and ffmpeg.
93
+ You need Python 3.9 or higher, and ffmpeg.
92
94
 
93
95
  ### ffmpeg
94
96
 
@@ -129,6 +131,54 @@ Or download this repository, then run `pip3 install .`.
129
131
 
130
132
  To later upgrade to the latest version, run `pip3 install --upgrade ffmpeg-normalize`.
131
133
 
134
+ ### Shell Completions
135
+
136
+ This tool provides shell completions for bash and zsh. To install them:
137
+
138
+ <!--
139
+ Note to self: Generate the shtab ones with:
140
+
141
+ shtab --shell=bash -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.bash
142
+ shtab --shell=zsh -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.zsh
143
+
144
+ but these are not properly working yet.
145
+ -->
146
+
147
+ #### Bash
148
+
149
+ 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.
150
+
151
+ ```bash
152
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
153
+ -o /usr/local/etc/bash_completion.d/ffmpeg-normalize
154
+ ```
155
+
156
+ Without bash-completion, you can manually install the completion script:
157
+
158
+ ```bash
159
+ # create completions directory if it doesn't exist
160
+ mkdir -p ~/.bash_completions.d
161
+
162
+ # download and install completion script
163
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
164
+ -o ~/.bash_completions.d/ffmpeg-normalize
165
+
166
+ # source it in your ~/.bashrc
167
+ echo 'source ~/.bash_completions.d/ffmpeg-normalize' >> ~/.bashrc
168
+ ```
169
+
170
+ #### Zsh
171
+
172
+ Download the completion script and place it in the default `site-functions` directory:
173
+
174
+ ```bash
175
+ curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize.zsh \
176
+ -o /usr/local/share/zsh/site-functions/
177
+ ```
178
+
179
+ You may choose any other directory that is in your `$FPATH` variable.
180
+ Make sure your `.zshrc` file contains `autoload -Uz compinit && compinit`.
181
+
132
182
  ## Usage with Docker
133
183
 
134
184
  You can use the pre-built image from Docker Hub:
@@ -280,10 +330,7 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
280
330
 
281
331
  - `--keep-lra-above-loudness-range-target`: Keep input loudness range above loudness range target.
282
332
 
283
- - `LOUDNESS_RANGE_TARGET` for input loudness range `<= LOUDNESS_RANGE_TARGET` or
284
- - keep input loudness range target above `LOUDNESS_RANGE_TARGET`.
285
-
286
- as alternative to `--keep-loudness-range-target` to allow for linear normalization.
333
+ Can be used as an alternative to `--keep-loudness-range-target` to allow for linear normalization.
287
334
 
288
335
  - `-tp TRUE_PEAK, --true-peak TRUE_PEAK`: EBU Maximum True Peak in dBTP (default: -2.0).
289
336
 
@@ -295,6 +342,16 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
295
342
 
296
343
  Range is -99.0 - +99.0.
297
344
 
345
+ - `--lower-only`: Whether the audio should not increase in loudness.
346
+
347
+ 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.
348
+
349
+ - `--auto-lower-loudness-target`: Automatically lower EBU Integrated Loudness Target.
350
+
351
+ Automatically lower EBU Integrated Loudness Target to prevent falling back to dynamic filtering.
352
+
353
+ Makes sure target loudness is lower than measured loudness minus peak loudness (input_i - input_tp) by a small amount.
354
+
298
355
  - `--dual-mono`: Treat mono input files as "dual-mono".
299
356
 
300
357
  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.
@@ -303,7 +360,7 @@ For more information on the options (`[options]`) available, run `ffmpeg-normali
303
360
 
304
361
  Instead of applying linear EBU R128 normalization, choose a dynamic normalization. This is not usually recommended.
305
362
 
306
- Dynamic mode will automatically change the sample rate to 192 kHz. Use -ar/--sample-rate to specify a different output sample rate.
363
+ Dynamic mode will automatically change the sample rate to 192 kHz. Use `-ar`/`--sample-rate` to specify a different output sample rate.
307
364
 
308
365
  ### Audio Encoding
309
366
 
@@ -405,6 +462,16 @@ For more information see the [API documentation](https://htmlpreview.github.io/?
405
462
 
406
463
  ## FAQ
407
464
 
465
+ ### My output file is too large?
466
+
467
+ This is because the default output codec is PCM, which is uncompressed. If you want to reduce the file size, you can specify an audio codec with `-c:a` (e.g., `-c:a aac` for ffmpeg's built-in AAC encoder), and optionally a bitrate with `-b:a`.
468
+
469
+ For example:
470
+
471
+ ```bash
472
+ ffmpeg-normalize input.wav -o output.m4a -c:a aac -b:a 192k
473
+ ```
474
+
408
475
  ### What options should I choose for the EBU R128 filter? What is linear and dynamic mode?
409
476
 
410
477
  EBU R128 is a method for normalizing audio loudness across different tracks or programs. It works by analyzing the audio content and adjusting it to meet specific loudness targets. The main components are:
@@ -429,7 +496,11 @@ For most cases, linear mode is recommended. Dynamic mode should only be used whe
429
496
 
430
497
  * 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.
431
498
 
432
- 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!
499
+ 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:
500
+
501
+ - 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.
502
+ - Similarly, the `--keep-loudness-range-target` option can be used to keep the input loudness range target.
503
+ - The `--lower-only` option can be used to skip the normalization pass completely if the measured loudness is lower than the target loudness.
433
504
 
434
505
  ### The program doesn't work because the "loudnorm" filter can't be found
435
506
 
@@ -548,6 +619,7 @@ If you found this program useful and feel like giving back, feel free to send a
548
619
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/psavva"><img src="https://avatars.githubusercontent.com/u/1454758?v=4?s=100" width="100px;" alt="Panayiotis Savva"/><br /><sub><b>Panayiotis Savva</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=psavva" title="Code">💻</a></td>
549
620
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/HighMans"><img src="https://avatars.githubusercontent.com/u/42877729?v=4?s=100" width="100px;" alt="HighMans"/><br /><sub><b>HighMans</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=HighMans" title="Code">💻</a></td>
550
621
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/kanjieater"><img src="https://avatars.githubusercontent.com/u/32607317?v=4?s=100" width="100px;" alt="kanjieater"/><br /><sub><b>kanjieater</b></sub></a><br /><a href="#ideas-kanjieater" title="Ideas, Planning, & Feedback">🤔</a></td>
622
+ <td align="center" valign="top" width="14.28%"><a href="https://ahmetsait.com/"><img src="https://avatars.githubusercontent.com/u/8372246?v=4?s=100" width="100px;" alt="Ahmet Sait"/><br /><sub><b>Ahmet Sait</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=ahmetsait" title="Code">💻</a></td>
551
623
  </tr>
552
624
  </tbody>
553
625
  <tfoot>
@@ -595,6 +667,44 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
595
667
  # Changelog
596
668
 
597
669
 
670
+ ## v1.31.0 (2024-12-15)
671
+
672
+ * Update docs and completions.
673
+
674
+ * Implement `--auto-lower-loudness-target`
675
+
676
+ * Fix deprecations and mypy --strict errors.
677
+
678
+ * Feat: add completions.
679
+
680
+ * Docs: update explainer.
681
+
682
+ * Docs: update docs to include lower-only.
683
+
684
+
685
+ ## v1.30.0 (2024-11-22)
686
+
687
+ * Change lower-only message to warning.
688
+
689
+ * Make setup name PEP 625 compliant.
690
+
691
+ * Docs: add @ahmetsait as a contributor.
692
+
693
+ * Implement `--lower-only`
694
+
695
+ * Fix: `--print-stats` only outputs the last stream.
696
+
697
+ * More robust `loudnorm` output parsing.
698
+
699
+ * Remove unnecessary conversions.
700
+
701
+ * Update .editorconfig.
702
+
703
+ * Remove python 3.8, add python 3.12, 3.13.
704
+
705
+ * Add README on file size.
706
+
707
+
598
708
  ## v1.29.2 (2024-11-18)
599
709
 
600
710
  * Fix: show percentage with two decimal digits in progress.
@@ -19,7 +19,7 @@ with open(path.join(here, "CHANGELOG.md"), encoding="utf8") as f:
19
19
  history = f.read()
20
20
 
21
21
  setup(
22
- name="ffmpeg-normalize",
22
+ name="ffmpeg_normalize",
23
23
  version=version,
24
24
  description="Normalize audio via ffmpeg",
25
25
  long_description=long_description + "\n\n" + history,
@@ -50,13 +50,13 @@ setup(
50
50
  "License :: OSI Approved :: MIT License",
51
51
  "Natural Language :: English",
52
52
  "Programming Language :: Python :: 3",
53
- "Programming Language :: Python :: 3.8",
54
53
  "Programming Language :: Python :: 3.9",
55
54
  "Programming Language :: Python :: 3.10",
56
55
  "Programming Language :: Python :: 3.11",
57
56
  "Programming Language :: Python :: 3.12",
57
+ "Programming Language :: Python :: 3.13",
58
58
  ],
59
- python_requires=">=3.8",
59
+ python_requires=">=3.9",
60
60
  entry_points={
61
61
  "console_scripts": ["ffmpeg-normalize = ffmpeg_normalize.__main__:main"]
62
62
  },
@@ -1 +0,0 @@
1
- __version__ = "1.29.2"