ffmpeg-normalize 1.29.2__tar.gz → 1.30.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.
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/CHANGELOG.md +23 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/PKG-INFO +41 -6
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/README.md +14 -2
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/__main__.py +15 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/_ffmpeg_normalize.py +8 -2
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/_media_file.py +49 -20
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/_streams.py +63 -90
- ffmpeg_normalize-1.30.0/ffmpeg_normalize/_version.py +1 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/PKG-INFO +40 -5
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/setup.py +3 -3
- ffmpeg-normalize-1.29.2/ffmpeg_normalize/_version.py +0 -1
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/LICENSE +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/__init__.py +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/_cmd_utils.py +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/_errors.py +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/_logger.py +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize/py.typed +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/SOURCES.txt +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/dependency_links.txt +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/entry_points.txt +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/not-zip-safe +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/requires.txt +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/top_level.txt +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/setup.cfg +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/test/out.mp4 +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/test/test.mp4 +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/test/test.py +0 -0
- {ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/test/test.wav +0 -0
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v1.30.0 (2024-11-22)
|
|
5
|
+
|
|
6
|
+
* Change lower-only message to warning.
|
|
7
|
+
|
|
8
|
+
* Make setup name PEP 625 compliant.
|
|
9
|
+
|
|
10
|
+
* Docs: add @ahmetsait as a contributor.
|
|
11
|
+
|
|
12
|
+
* Implement `--lower-only`
|
|
13
|
+
|
|
14
|
+
* Fix: `--print-stats` only outputs the last stream.
|
|
15
|
+
|
|
16
|
+
* More robust `loudnorm` output parsing.
|
|
17
|
+
|
|
18
|
+
* Remove unnecessary conversions.
|
|
19
|
+
|
|
20
|
+
* Update .editorconfig.
|
|
21
|
+
|
|
22
|
+
* Remove python 3.8, add python 3.12, 3.13.
|
|
23
|
+
|
|
24
|
+
* Add README on file size.
|
|
25
|
+
|
|
26
|
+
|
|
4
27
|
## v1.29.2 (2024-11-18)
|
|
5
28
|
|
|
6
29
|
* Fix: show percentage with two decimal digits in progress.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
|
-
Name:
|
|
3
|
-
Version: 1.
|
|
2
|
+
Name: ffmpeg_normalize
|
|
3
|
+
Version: 1.30.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
|
-
|
|
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
|

|
|
32
32
|
|
|
33
33
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
34
|
-
[](#contributors-)
|
|
35
35
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
36
36
|
|
|
37
37
|
A utility for batch-normalizing audio using ffmpeg.
|
|
@@ -69,6 +69,7 @@ Read on for more info.
|
|
|
69
69
|
- [Environment Variables](#environment-variables)
|
|
70
70
|
- [API](#api)
|
|
71
71
|
- [FAQ](#faq)
|
|
72
|
+
- [My output file is too large?](#my-output-file-is-too-large)
|
|
72
73
|
- [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
74
|
- [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
75
|
- [Should I use this to normalize my music collection?](#should-i-use-this-to-normalize-my-music-collection)
|
|
@@ -88,7 +89,7 @@ Read on for more info.
|
|
|
88
89
|
|
|
89
90
|
## Requirements
|
|
90
91
|
|
|
91
|
-
You need Python 3.
|
|
92
|
+
You need Python 3.9 or higher, and ffmpeg.
|
|
92
93
|
|
|
93
94
|
### ffmpeg
|
|
94
95
|
|
|
@@ -405,6 +406,16 @@ For more information see the [API documentation](https://htmlpreview.github.io/?
|
|
|
405
406
|
|
|
406
407
|
## FAQ
|
|
407
408
|
|
|
409
|
+
### My output file is too large?
|
|
410
|
+
|
|
411
|
+
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`.
|
|
412
|
+
|
|
413
|
+
For example:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
ffmpeg-normalize input.wav -o output.m4a -c:a aac -b:a 192k
|
|
417
|
+
```
|
|
418
|
+
|
|
408
419
|
### What options should I choose for the EBU R128 filter? What is linear and dynamic mode?
|
|
409
420
|
|
|
410
421
|
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:
|
|
@@ -548,6 +559,7 @@ If you found this program useful and feel like giving back, feel free to send a
|
|
|
548
559
|
<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
560
|
<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
561
|
<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>
|
|
562
|
+
<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
563
|
</tr>
|
|
552
564
|
</tbody>
|
|
553
565
|
<tfoot>
|
|
@@ -595,6 +607,29 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
595
607
|
# Changelog
|
|
596
608
|
|
|
597
609
|
|
|
610
|
+
## v1.30.0 (2024-11-22)
|
|
611
|
+
|
|
612
|
+
* Change lower-only message to warning.
|
|
613
|
+
|
|
614
|
+
* Make setup name PEP 625 compliant.
|
|
615
|
+
|
|
616
|
+
* Docs: add @ahmetsait as a contributor.
|
|
617
|
+
|
|
618
|
+
* Implement `--lower-only`
|
|
619
|
+
|
|
620
|
+
* Fix: `--print-stats` only outputs the last stream.
|
|
621
|
+
|
|
622
|
+
* More robust `loudnorm` output parsing.
|
|
623
|
+
|
|
624
|
+
* Remove unnecessary conversions.
|
|
625
|
+
|
|
626
|
+
* Update .editorconfig.
|
|
627
|
+
|
|
628
|
+
* Remove python 3.8, add python 3.12, 3.13.
|
|
629
|
+
|
|
630
|
+
* Add README on file size.
|
|
631
|
+
|
|
632
|
+
|
|
598
633
|
## v1.29.2 (2024-11-18)
|
|
599
634
|
|
|
600
635
|
* Fix: show percentage with two decimal digits in progress.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|

|
|
6
6
|
|
|
7
7
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
8
|
-
[](#contributors-)
|
|
9
9
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
10
10
|
|
|
11
11
|
A utility for batch-normalizing audio using ffmpeg.
|
|
@@ -43,6 +43,7 @@ Read on for more info.
|
|
|
43
43
|
- [Environment Variables](#environment-variables)
|
|
44
44
|
- [API](#api)
|
|
45
45
|
- [FAQ](#faq)
|
|
46
|
+
- [My output file is too large?](#my-output-file-is-too-large)
|
|
46
47
|
- [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
48
|
- [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
49
|
- [Should I use this to normalize my music collection?](#should-i-use-this-to-normalize-my-music-collection)
|
|
@@ -62,7 +63,7 @@ Read on for more info.
|
|
|
62
63
|
|
|
63
64
|
## Requirements
|
|
64
65
|
|
|
65
|
-
You need Python 3.
|
|
66
|
+
You need Python 3.9 or higher, and ffmpeg.
|
|
66
67
|
|
|
67
68
|
### ffmpeg
|
|
68
69
|
|
|
@@ -379,6 +380,16 @@ For more information see the [API documentation](https://htmlpreview.github.io/?
|
|
|
379
380
|
|
|
380
381
|
## FAQ
|
|
381
382
|
|
|
383
|
+
### My output file is too large?
|
|
384
|
+
|
|
385
|
+
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`.
|
|
386
|
+
|
|
387
|
+
For example:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
ffmpeg-normalize input.wav -o output.m4a -c:a aac -b:a 192k
|
|
391
|
+
```
|
|
392
|
+
|
|
382
393
|
### What options should I choose for the EBU R128 filter? What is linear and dynamic mode?
|
|
383
394
|
|
|
384
395
|
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:
|
|
@@ -522,6 +533,7 @@ If you found this program useful and feel like giving back, feel free to send a
|
|
|
522
533
|
<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
534
|
<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
535
|
<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>
|
|
536
|
+
<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
537
|
</tr>
|
|
526
538
|
</tbody>
|
|
527
539
|
<tfoot>
|
|
@@ -235,6 +235,20 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
235
235
|
default=0.0,
|
|
236
236
|
)
|
|
237
237
|
|
|
238
|
+
group_ebu.add_argument(
|
|
239
|
+
"--lower-only",
|
|
240
|
+
action="store_true",
|
|
241
|
+
help=textwrap.dedent(
|
|
242
|
+
"""\
|
|
243
|
+
Whether the audio should not increase in loudness.
|
|
244
|
+
|
|
245
|
+
If the measured loudness from the first pass is lower than the target
|
|
246
|
+
loudness then normalization pass will be skipped for the measured audio
|
|
247
|
+
source.
|
|
248
|
+
"""
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
|
|
238
252
|
group_ebu.add_argument(
|
|
239
253
|
"--dual-mono",
|
|
240
254
|
action="store_true",
|
|
@@ -514,6 +528,7 @@ def main() -> None:
|
|
|
514
528
|
keep_lra_above_loudness_range_target=cli_args.keep_lra_above_loudness_range_target,
|
|
515
529
|
true_peak=cli_args.true_peak,
|
|
516
530
|
offset=cli_args.offset,
|
|
531
|
+
lower_only=cli_args.lower_only,
|
|
517
532
|
dual_mono=cli_args.dual_mono,
|
|
518
533
|
dynamic=cli_args.dynamic,
|
|
519
534
|
audio_codec=cli_args.audio_codec,
|
|
@@ -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,7 @@ 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.
|
|
61
64
|
dual_mono (bool, optional): Dual mono. Defaults to False.
|
|
62
65
|
dynamic (bool, optional): Dynamic. Defaults to False.
|
|
63
66
|
audio_codec (str, optional): Audio codec. Defaults to "pcm_s16le".
|
|
@@ -94,6 +97,7 @@ class FFmpegNormalize:
|
|
|
94
97
|
keep_lra_above_loudness_range_target: bool = False,
|
|
95
98
|
true_peak: float = -2.0,
|
|
96
99
|
offset: float = 0.0,
|
|
100
|
+
lower_only: bool = False,
|
|
97
101
|
dual_mono: bool = False,
|
|
98
102
|
dynamic: bool = False,
|
|
99
103
|
audio_codec: str = "pcm_s16le",
|
|
@@ -164,6 +168,7 @@ class FFmpegNormalize:
|
|
|
164
168
|
|
|
165
169
|
self.true_peak = check_range(true_peak, -9, 0, name="true_peak")
|
|
166
170
|
self.offset = check_range(offset, -99, 99, name="offset")
|
|
171
|
+
self.lower_only = lower_only
|
|
167
172
|
|
|
168
173
|
# Ensure library user is passing correct types
|
|
169
174
|
assert isinstance(dual_mono, bool), "dual_mono must be bool"
|
|
@@ -254,5 +259,6 @@ class FFmpegNormalize:
|
|
|
254
259
|
|
|
255
260
|
_logger.info(f"Normalized file written to {media_file.output_file}")
|
|
256
261
|
|
|
257
|
-
if self.print_stats
|
|
258
|
-
|
|
262
|
+
if self.print_stats:
|
|
263
|
+
json.dump(list(chain.from_iterable(media_file.get_stats() for media_file in self.media_files)), sys.stdout, indent=4)
|
|
264
|
+
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
|
|
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
|
-
|
|
260
|
-
|
|
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.warn(
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
@@ -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
|
-
|
|
324
|
-
|
|
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
|
|
332
|
-
) ->
|
|
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]
|
|
368
|
-
) ->
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
"""
|
|
@@ -515,19 +488,19 @@ class AudioStream(MediaStream):
|
|
|
515
488
|
"lra": self.media_file.ffmpeg_normalize.loudness_range_target,
|
|
516
489
|
"tp": self.media_file.ffmpeg_normalize.true_peak,
|
|
517
490
|
"offset": self._constrain(
|
|
518
|
-
|
|
491
|
+
stats["target_offset"], -99, 99, name="target_offset"
|
|
519
492
|
),
|
|
520
493
|
"measured_i": self._constrain(
|
|
521
|
-
|
|
494
|
+
stats["input_i"], -99, 0, name="input_i"
|
|
522
495
|
),
|
|
523
496
|
"measured_lra": self._constrain(
|
|
524
|
-
|
|
497
|
+
stats["input_lra"], 0, 99, name="input_lra"
|
|
525
498
|
),
|
|
526
499
|
"measured_tp": self._constrain(
|
|
527
|
-
|
|
500
|
+
stats["input_tp"], -99, 99, name="input_tp"
|
|
528
501
|
),
|
|
529
502
|
"measured_thresh": self._constrain(
|
|
530
|
-
|
|
503
|
+
stats["input_thresh"], -99, 0, name="input_thresh"
|
|
531
504
|
),
|
|
532
505
|
"linear": "false" if self.media_file.ffmpeg_normalize.dynamic else "true",
|
|
533
506
|
"print_format": "json",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.30.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ffmpeg-normalize
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.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
|
-
|
|
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
|

|
|
32
32
|
|
|
33
33
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
34
|
-
[](#contributors-)
|
|
35
35
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
36
36
|
|
|
37
37
|
A utility for batch-normalizing audio using ffmpeg.
|
|
@@ -69,6 +69,7 @@ Read on for more info.
|
|
|
69
69
|
- [Environment Variables](#environment-variables)
|
|
70
70
|
- [API](#api)
|
|
71
71
|
- [FAQ](#faq)
|
|
72
|
+
- [My output file is too large?](#my-output-file-is-too-large)
|
|
72
73
|
- [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
74
|
- [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
75
|
- [Should I use this to normalize my music collection?](#should-i-use-this-to-normalize-my-music-collection)
|
|
@@ -88,7 +89,7 @@ Read on for more info.
|
|
|
88
89
|
|
|
89
90
|
## Requirements
|
|
90
91
|
|
|
91
|
-
You need Python 3.
|
|
92
|
+
You need Python 3.9 or higher, and ffmpeg.
|
|
92
93
|
|
|
93
94
|
### ffmpeg
|
|
94
95
|
|
|
@@ -405,6 +406,16 @@ For more information see the [API documentation](https://htmlpreview.github.io/?
|
|
|
405
406
|
|
|
406
407
|
## FAQ
|
|
407
408
|
|
|
409
|
+
### My output file is too large?
|
|
410
|
+
|
|
411
|
+
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`.
|
|
412
|
+
|
|
413
|
+
For example:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
ffmpeg-normalize input.wav -o output.m4a -c:a aac -b:a 192k
|
|
417
|
+
```
|
|
418
|
+
|
|
408
419
|
### What options should I choose for the EBU R128 filter? What is linear and dynamic mode?
|
|
409
420
|
|
|
410
421
|
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:
|
|
@@ -548,6 +559,7 @@ If you found this program useful and feel like giving back, feel free to send a
|
|
|
548
559
|
<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
560
|
<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
561
|
<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>
|
|
562
|
+
<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
563
|
</tr>
|
|
552
564
|
</tbody>
|
|
553
565
|
<tfoot>
|
|
@@ -595,6 +607,29 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
595
607
|
# Changelog
|
|
596
608
|
|
|
597
609
|
|
|
610
|
+
## v1.30.0 (2024-11-22)
|
|
611
|
+
|
|
612
|
+
* Change lower-only message to warning.
|
|
613
|
+
|
|
614
|
+
* Make setup name PEP 625 compliant.
|
|
615
|
+
|
|
616
|
+
* Docs: add @ahmetsait as a contributor.
|
|
617
|
+
|
|
618
|
+
* Implement `--lower-only`
|
|
619
|
+
|
|
620
|
+
* Fix: `--print-stats` only outputs the last stream.
|
|
621
|
+
|
|
622
|
+
* More robust `loudnorm` output parsing.
|
|
623
|
+
|
|
624
|
+
* Remove unnecessary conversions.
|
|
625
|
+
|
|
626
|
+
* Update .editorconfig.
|
|
627
|
+
|
|
628
|
+
* Remove python 3.8, add python 3.12, 3.13.
|
|
629
|
+
|
|
630
|
+
* Add README on file size.
|
|
631
|
+
|
|
632
|
+
|
|
598
633
|
## v1.29.2 (2024-11-18)
|
|
599
634
|
|
|
600
635
|
* 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="
|
|
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.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ffmpeg-normalize-1.29.2 → ffmpeg_normalize-1.30.0}/ffmpeg_normalize.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|