ffmpeg-normalize 1.41.1__tar.gz → 1.42.1__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.41.1 → ffmpeg_normalize-1.42.1}/PKG-INFO +18 -3
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/README.md +16 -2
- ffmpeg_normalize-1.42.1/pyproject.toml +69 -0
- ffmpeg_normalize-1.41.1/pyproject.toml → ffmpeg_normalize-1.42.1/pyproject.toml.orig +2 -1
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/__main__.py +18 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/_cmd_utils.py +11 -8
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/_ffmpeg_normalize.py +10 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/_media_file.py +124 -58
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/LICENSE.md +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/__init__.py +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/_errors.py +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/_logger.py +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/_presets.py +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/_streams.py +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/data/presets/music.json +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/data/presets/podcast.json +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/data/presets/streaming-video.json +0 -0
- {ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ffmpeg-normalize
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.42.1
|
|
4
4
|
Summary: Normalize audio via ffmpeg
|
|
5
5
|
Keywords: ffmpeg,normalize,audio
|
|
6
6
|
Author: Werner Robitza
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
22
23
|
Requires-Dist: tqdm>=4.64.1
|
|
23
24
|
Requires-Dist: colorama>=0.4.6 ; sys_platform == 'win32'
|
|
24
25
|
Requires-Dist: ffmpeg-progress-yield>=1.0.1
|
|
@@ -29,14 +30,18 @@ Project-URL: Homepage, https://github.com/slhck/ffmpeg-normalize
|
|
|
29
30
|
Project-URL: Repository, https://github.com/slhck/ffmpeg-normalize
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
<h1 align="center">ffmpeg-normalize</h1>
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="icon.png" alt="ffmpeg-normalize logo" width="160" height="160">
|
|
37
|
+
</p>
|
|
33
38
|
|
|
34
39
|
[](https://pypi.org/project/ffmpeg-normalize)
|
|
35
40
|
[](https://hub.docker.com/r/slhck/ffmpeg-normalize)
|
|
36
41
|

|
|
37
42
|
|
|
38
43
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
39
|
-
[](#contributors-)
|
|
40
45
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
41
46
|
|
|
42
47
|
A utility for batch-normalizing audio using ffmpeg.
|
|
@@ -50,6 +55,7 @@ This program normalizes media files to a certain loudness level using the EBU R1
|
|
|
50
55
|
- Peak normalization — Adjust audio to a specific peak level
|
|
51
56
|
- Selective audio stream normalization — Normalize specific audio streams or only default streams
|
|
52
57
|
- Skip files already at target — Avoid re-encoding files already within a threshold of the target level
|
|
58
|
+
- Self-documenting output — Optionally record the effective encoder settings as an `ENCODER_SETTINGS` tag
|
|
53
59
|
- Video file support — Process video files while preserving video streams
|
|
54
60
|
- Docker support — Run via Docker container
|
|
55
61
|
- Python API — Use programmatically in your Python projects
|
|
@@ -65,6 +71,14 @@ This program normalizes media files to a certain loudness level using the EBU R1
|
|
|
65
71
|
|
|
66
72
|
## 🆕 What's New
|
|
67
73
|
|
|
74
|
+
- Version 1.42.0 can optionally write an **`ENCODER_SETTINGS`** tag on each normalized audio stream via `--write-encoder-settings`. The tag holds an ffmpeg-command-equivalent string — the audio codec, bitrate, sample rate and channels, plus the normalization filter including the measured loudnorm values — so a normalized file documents how it was produced. This is a Matroska/WebM convention and has no effect with `--metadata-disable`.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
ffmpeg-normalize input.wav -c:a libopus -b:a 128k --write-encoder-settings -o output.mkv
|
|
80
|
+
```
|
|
81
|
+
|
|
68
82
|
- Version 1.41.0 automatically picks the correct output audio codec for the output container, so you no longer need to specify `-c:a`/`--audio-codec` unless you want to override the default. PCM is chosen for containers that support it; others will use teh default that ffmpeg picks. See [the usage guide](https://slhck.info/ffmpeg-normalize/usage/file-input-output/#how-the-output-audio-codec-is-chosen) for details.
|
|
69
83
|
|
|
70
84
|
- Version 1.40.0 can optionally **skip files that are already at the target level** via `--threshold` (e.g. `--threshold 0.5`, disabled by default). Such files are copied through unchanged instead of being re-encoded. The `--print-stats` output now includes a per-file `status` (`normalized`, `skipped`, or `error`, plus an `error` message on failure), and the exit code is non-zero if any file failed to process, so a script can tell what happened to each file.
|
|
@@ -169,6 +183,7 @@ The only reason this project exists in its current form is because [@benjaoming]
|
|
|
169
183
|
<tr>
|
|
170
184
|
<td align="center" valign="top" width="14.28%"><a href="https://davidbern.com/"><img src="https://avatars.githubusercontent.com/u/371066?v=4?s=100" width="100px;" alt="David Bern"/><br /><sub><b>David Bern</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=odie5533" title="Code">💻</a></td>
|
|
171
185
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/randompersona1"><img src="https://avatars.githubusercontent.com/u/74961116?v=4?s=100" width="100px;" alt="randompersona1"/><br /><sub><b>randompersona1</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=randompersona1" title="Code">💻</a></td>
|
|
186
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ppetr"><img src="https://avatars.githubusercontent.com/u/163061?v=4?s=100" width="100px;" alt="Petr"/><br /><sub><b>Petr</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=ppetr" title="Code">💻</a></td>
|
|
172
187
|
</tr>
|
|
173
188
|
</tbody>
|
|
174
189
|
<tfoot>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">ffmpeg-normalize</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="icon.png" alt="ffmpeg-normalize logo" width="160" height="160">
|
|
5
|
+
</p>
|
|
2
6
|
|
|
3
7
|
[](https://pypi.org/project/ffmpeg-normalize)
|
|
4
8
|
[](https://hub.docker.com/r/slhck/ffmpeg-normalize)
|
|
5
9
|

|
|
6
10
|
|
|
7
11
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
8
|
-
[](#contributors-)
|
|
9
13
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
10
14
|
|
|
11
15
|
A utility for batch-normalizing audio using ffmpeg.
|
|
@@ -19,6 +23,7 @@ This program normalizes media files to a certain loudness level using the EBU R1
|
|
|
19
23
|
- Peak normalization — Adjust audio to a specific peak level
|
|
20
24
|
- Selective audio stream normalization — Normalize specific audio streams or only default streams
|
|
21
25
|
- Skip files already at target — Avoid re-encoding files already within a threshold of the target level
|
|
26
|
+
- Self-documenting output — Optionally record the effective encoder settings as an `ENCODER_SETTINGS` tag
|
|
22
27
|
- Video file support — Process video files while preserving video streams
|
|
23
28
|
- Docker support — Run via Docker container
|
|
24
29
|
- Python API — Use programmatically in your Python projects
|
|
@@ -34,6 +39,14 @@ This program normalizes media files to a certain loudness level using the EBU R1
|
|
|
34
39
|
|
|
35
40
|
## 🆕 What's New
|
|
36
41
|
|
|
42
|
+
- Version 1.42.0 can optionally write an **`ENCODER_SETTINGS`** tag on each normalized audio stream via `--write-encoder-settings`. The tag holds an ffmpeg-command-equivalent string — the audio codec, bitrate, sample rate and channels, plus the normalization filter including the measured loudnorm values — so a normalized file documents how it was produced. This is a Matroska/WebM convention and has no effect with `--metadata-disable`.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
ffmpeg-normalize input.wav -c:a libopus -b:a 128k --write-encoder-settings -o output.mkv
|
|
48
|
+
```
|
|
49
|
+
|
|
37
50
|
- Version 1.41.0 automatically picks the correct output audio codec for the output container, so you no longer need to specify `-c:a`/`--audio-codec` unless you want to override the default. PCM is chosen for containers that support it; others will use teh default that ffmpeg picks. See [the usage guide](https://slhck.info/ffmpeg-normalize/usage/file-input-output/#how-the-output-audio-codec-is-chosen) for details.
|
|
38
51
|
|
|
39
52
|
- Version 1.40.0 can optionally **skip files that are already at the target level** via `--threshold` (e.g. `--threshold 0.5`, disabled by default). Such files are copied through unchanged instead of being re-encoded. The `--print-stats` output now includes a per-file `status` (`normalized`, `skipped`, or `error`, plus an `error` message on failure), and the exit code is non-zero if any file failed to process, so a script can tell what happened to each file.
|
|
@@ -138,6 +151,7 @@ The only reason this project exists in its current form is because [@benjaoming]
|
|
|
138
151
|
<tr>
|
|
139
152
|
<td align="center" valign="top" width="14.28%"><a href="https://davidbern.com/"><img src="https://avatars.githubusercontent.com/u/371066?v=4?s=100" width="100px;" alt="David Bern"/><br /><sub><b>David Bern</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=odie5533" title="Code">💻</a></td>
|
|
140
153
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/randompersona1"><img src="https://avatars.githubusercontent.com/u/74961116?v=4?s=100" width="100px;" alt="randompersona1"/><br /><sub><b>randompersona1</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=randompersona1" title="Code">💻</a></td>
|
|
154
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ppetr"><img src="https://avatars.githubusercontent.com/u/163061?v=4?s=100" width="100px;" alt="Petr"/><br /><sub><b>Petr</b></sub></a><br /><a href="https://github.com/slhck/ffmpeg-normalize/commits?author=ppetr" title="Code">💻</a></td>
|
|
141
155
|
</tr>
|
|
142
156
|
</tbody>
|
|
143
157
|
<tfoot>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.8.14,<1.0.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ffmpeg-normalize"
|
|
7
|
+
version = "1.42.1"
|
|
8
|
+
description = "Normalize audio via ffmpeg"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE.md"]
|
|
12
|
+
keywords = [
|
|
13
|
+
"ffmpeg",
|
|
14
|
+
"normalize",
|
|
15
|
+
"audio",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
21
|
+
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
|
22
|
+
"Topic :: Multimedia :: Sound/Audio :: Conversion",
|
|
23
|
+
"Natural Language :: English",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
30
|
+
"Programming Language :: Python :: 3.15",
|
|
31
|
+
]
|
|
32
|
+
requires-python = ">=3.10"
|
|
33
|
+
dependencies = [
|
|
34
|
+
"tqdm>=4.64.1",
|
|
35
|
+
"colorama>=0.4.6; platform_system=='Windows'",
|
|
36
|
+
"ffmpeg-progress-yield>=1.0.1",
|
|
37
|
+
"colorlog==6.7.0",
|
|
38
|
+
"mutagen>=1.47.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[[project.authors]]
|
|
42
|
+
name = "Werner Robitza"
|
|
43
|
+
email = "werner.robitza@gmail.com"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/slhck/ffmpeg-normalize"
|
|
47
|
+
Repository = "https://github.com/slhck/ffmpeg-normalize"
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
ffmpeg-normalize = "ffmpeg_normalize.__main__:main"
|
|
51
|
+
|
|
52
|
+
[tool.uv_build]
|
|
53
|
+
src-layout = true
|
|
54
|
+
|
|
55
|
+
[tool.uv_build.package-data]
|
|
56
|
+
ffmpeg_normalize = ["data/**/*.json"]
|
|
57
|
+
|
|
58
|
+
[tool.ty.rules]
|
|
59
|
+
invalid-argument-type = "warn"
|
|
60
|
+
too-many-positional-arguments = "warn"
|
|
61
|
+
|
|
62
|
+
[dependency-groups]
|
|
63
|
+
dev = [
|
|
64
|
+
"pytest>=9.0.3",
|
|
65
|
+
"ruff>=0.12.11",
|
|
66
|
+
"types-tqdm",
|
|
67
|
+
"shtab>=1.7.0",
|
|
68
|
+
"ty>=0.0.24",
|
|
69
|
+
]
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ffmpeg-normalize"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.42.1"
|
|
8
8
|
description = "Normalize audio via ffmpeg"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -26,6 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: 3.12",
|
|
27
27
|
"Programming Language :: Python :: 3.13",
|
|
28
28
|
"Programming Language :: Python :: 3.14",
|
|
29
|
+
"Programming Language :: Python :: 3.15",
|
|
29
30
|
]
|
|
30
31
|
requires-python = ">=3.10"
|
|
31
32
|
dependencies = [
|
|
@@ -562,6 +562,23 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
562
562
|
action="store_true",
|
|
563
563
|
help="Do not write chapters to output",
|
|
564
564
|
)
|
|
565
|
+
group_vcodec.add_argument(
|
|
566
|
+
"--write-encoder-settings",
|
|
567
|
+
action="store_true",
|
|
568
|
+
help=textwrap.dedent(
|
|
569
|
+
"""\
|
|
570
|
+
Write the effective encoder settings as an ENCODER_SETTINGS tag on each
|
|
571
|
+
normalized audio stream.
|
|
572
|
+
|
|
573
|
+
The tag holds an ffmpeg-command-equivalent string with the audio codec,
|
|
574
|
+
the bitrate, sample rate and channel count (when set), plus the
|
|
575
|
+
normalization filter including the measured loudnorm values.
|
|
576
|
+
|
|
577
|
+
This is a Matroska/WebM tag convention; other containers may store it
|
|
578
|
+
differently. Has no effect together with `--metadata-disable`.
|
|
579
|
+
"""
|
|
580
|
+
),
|
|
581
|
+
)
|
|
565
582
|
|
|
566
583
|
group_format = parser.add_argument_group("Input/Output options")
|
|
567
584
|
group_format.add_argument(
|
|
@@ -744,6 +761,7 @@ def main() -> None:
|
|
|
744
761
|
subtitle_disable=cli_args.subtitle_disable,
|
|
745
762
|
metadata_disable=cli_args.metadata_disable,
|
|
746
763
|
chapters_disable=cli_args.chapters_disable,
|
|
764
|
+
write_encoder_settings=cli_args.write_encoder_settings,
|
|
747
765
|
extra_input_options=extra_input_options,
|
|
748
766
|
extra_output_options=extra_output_options,
|
|
749
767
|
output_format=cli_args.output_format,
|
|
@@ -9,7 +9,7 @@ import subprocess
|
|
|
9
9
|
import sys
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from shutil import which
|
|
12
|
-
from typing import Any, Iterator
|
|
12
|
+
from typing import Any, Iterator, cast
|
|
13
13
|
|
|
14
14
|
from ffmpeg_progress_yield import FfmpegProgress
|
|
15
15
|
|
|
@@ -161,13 +161,16 @@ class CommandRunner:
|
|
|
161
161
|
_logger.debug("Dry mode specified, not actually running command")
|
|
162
162
|
return self
|
|
163
163
|
|
|
164
|
-
p =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
p = cast(
|
|
165
|
+
subprocess.Popen[bytes],
|
|
166
|
+
subprocess.Popen(
|
|
167
|
+
cmd,
|
|
168
|
+
stdin=subprocess.PIPE, # Apply stdin isolation by creating separate pipe.
|
|
169
|
+
stdout=subprocess.PIPE,
|
|
170
|
+
stderr=subprocess.PIPE,
|
|
171
|
+
text=False,
|
|
172
|
+
**_get_ffmpeg_popen_kwargs(),
|
|
173
|
+
),
|
|
171
174
|
)
|
|
172
175
|
|
|
173
176
|
stdout_bytes, stderr_bytes = p.communicate()
|
{ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/_ffmpeg_normalize.py
RENAMED
|
@@ -82,6 +82,7 @@ class FFmpegNormalize:
|
|
|
82
82
|
subtitle_disable (bool, optional): Disable subtitles. Defaults to False.
|
|
83
83
|
metadata_disable (bool, optional): Disable metadata. Defaults to False.
|
|
84
84
|
chapters_disable (bool, optional): Disable chapters. Defaults to False.
|
|
85
|
+
write_encoder_settings (bool, optional): Write the effective encoder settings (codec, bitrate, sample rate, channels and the normalization filter including the measured loudnorm values) as an ENCODER_SETTINGS tag on each normalized audio stream. Defaults to False.
|
|
85
86
|
extra_input_options (list, optional): Extra input options. Defaults to None.
|
|
86
87
|
extra_output_options (list, optional): Extra output options. Defaults to None.
|
|
87
88
|
output_format (str, optional): Output format. Defaults to None.
|
|
@@ -129,6 +130,7 @@ class FFmpegNormalize:
|
|
|
129
130
|
"subtitle_disable": False,
|
|
130
131
|
"metadata_disable": False,
|
|
131
132
|
"chapters_disable": False,
|
|
133
|
+
"write_encoder_settings": False,
|
|
132
134
|
"extra_input_options": None,
|
|
133
135
|
"extra_output_options": None,
|
|
134
136
|
"output_format": None,
|
|
@@ -173,6 +175,7 @@ class FFmpegNormalize:
|
|
|
173
175
|
subtitle_disable: bool = False,
|
|
174
176
|
metadata_disable: bool = False,
|
|
175
177
|
chapters_disable: bool = False,
|
|
178
|
+
write_encoder_settings: bool = False,
|
|
176
179
|
extra_input_options: list[str] | None = None,
|
|
177
180
|
extra_output_options: list[str] | None = None,
|
|
178
181
|
output_format: str | None = None,
|
|
@@ -257,6 +260,13 @@ class FFmpegNormalize:
|
|
|
257
260
|
self.subtitle_disable = subtitle_disable
|
|
258
261
|
self.metadata_disable = metadata_disable
|
|
259
262
|
self.chapters_disable = chapters_disable
|
|
263
|
+
self.write_encoder_settings = write_encoder_settings
|
|
264
|
+
|
|
265
|
+
if self.write_encoder_settings and self.metadata_disable:
|
|
266
|
+
_logger.warning(
|
|
267
|
+
"write_encoder_settings has no effect together with metadata_disable; "
|
|
268
|
+
"no metadata will be written to the output."
|
|
269
|
+
)
|
|
260
270
|
|
|
261
271
|
self.extra_input_options = extra_input_options
|
|
262
272
|
self.extra_output_options = extra_output_options
|
|
@@ -99,6 +99,10 @@ class MediaFile:
|
|
|
99
99
|
self.streams: StreamDict = {"audio": {}, "video": {}, "subtitle": {}}
|
|
100
100
|
self.temp_file: Union[str, None] = None
|
|
101
101
|
self.batch_reference: float | None = None
|
|
102
|
+
# Second-pass filter chain per normalized audio stream, keyed by stream
|
|
103
|
+
# id. Populated by _get_audio_filter_cmd() and reused by
|
|
104
|
+
# _get_encoder_settings() so the normalization filter is not built twice.
|
|
105
|
+
self._stream_filter_chains: dict[int, list[str]] = {}
|
|
102
106
|
# Input (access, modification) times captured before processing, used
|
|
103
107
|
# when the keep_mtime option is enabled.
|
|
104
108
|
self.input_timestamps: tuple[float, float] | None = None
|
|
@@ -768,6 +772,75 @@ class MediaFile:
|
|
|
768
772
|
for _ in fun():
|
|
769
773
|
pass
|
|
770
774
|
|
|
775
|
+
def _get_stream_filter_chain(self, audio_stream: AudioStream) -> list[str]:
|
|
776
|
+
"""
|
|
777
|
+
Return the ordered list of ffmpeg audio filters applied to a single
|
|
778
|
+
stream in the second pass: the optional pre-filter, channel conversion,
|
|
779
|
+
the normalization filter itself (loudnorm for EBU, or the volume /
|
|
780
|
+
dynaudnorm filter for peak / RMS), and the optional post-filter.
|
|
781
|
+
|
|
782
|
+
Returns:
|
|
783
|
+
list[str]: the filters, in application order
|
|
784
|
+
"""
|
|
785
|
+
skip_normalization = False
|
|
786
|
+
if self.ffmpeg_normalize.lower_only:
|
|
787
|
+
if self.ffmpeg_normalize.normalization_type == "ebu":
|
|
788
|
+
if (
|
|
789
|
+
audio_stream.loudness_statistics["ebu_pass1"] is not None
|
|
790
|
+
and audio_stream.loudness_statistics["ebu_pass1"]["input_i"]
|
|
791
|
+
< self.ffmpeg_normalize.target_level
|
|
792
|
+
):
|
|
793
|
+
skip_normalization = True
|
|
794
|
+
elif self.ffmpeg_normalize.normalization_type == "peak":
|
|
795
|
+
if (
|
|
796
|
+
audio_stream.loudness_statistics["max"] is not None
|
|
797
|
+
and audio_stream.loudness_statistics["max"]
|
|
798
|
+
< self.ffmpeg_normalize.target_level
|
|
799
|
+
):
|
|
800
|
+
skip_normalization = True
|
|
801
|
+
elif self.ffmpeg_normalize.normalization_type == "rms":
|
|
802
|
+
if (
|
|
803
|
+
audio_stream.loudness_statistics["mean"] is not None
|
|
804
|
+
and audio_stream.loudness_statistics["mean"]
|
|
805
|
+
< self.ffmpeg_normalize.target_level
|
|
806
|
+
):
|
|
807
|
+
skip_normalization = True
|
|
808
|
+
|
|
809
|
+
if skip_normalization:
|
|
810
|
+
_logger.warning(
|
|
811
|
+
f"Stream {audio_stream.stream_id} had measured input loudness lower than target, skipping normalization."
|
|
812
|
+
)
|
|
813
|
+
normalization_filter = "acopy"
|
|
814
|
+
else:
|
|
815
|
+
if self.ffmpeg_normalize.normalization_type == "ebu":
|
|
816
|
+
normalization_filter = audio_stream.get_second_pass_opts_ebu(
|
|
817
|
+
batch_reference=self.batch_reference
|
|
818
|
+
)
|
|
819
|
+
else:
|
|
820
|
+
normalization_filter = audio_stream.get_second_pass_opts_peakrms(
|
|
821
|
+
batch_reference=self.batch_reference
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
filter_chain = []
|
|
825
|
+
|
|
826
|
+
if self.ffmpeg_normalize.pre_filter:
|
|
827
|
+
filter_chain.append(self.ffmpeg_normalize.pre_filter)
|
|
828
|
+
|
|
829
|
+
# Apply channel conversion before normalization so that the
|
|
830
|
+
# normalization filter operates on the same channel layout that
|
|
831
|
+
# was measured in the first pass. See issue #316.
|
|
832
|
+
if self.ffmpeg_normalize.audio_channels:
|
|
833
|
+
filter_chain.append(
|
|
834
|
+
f"aformat=sample_fmts=fltp:channel_layouts={self.ffmpeg_normalize.audio_channels}c"
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
filter_chain.append(normalization_filter)
|
|
838
|
+
|
|
839
|
+
if self.ffmpeg_normalize.post_filter:
|
|
840
|
+
filter_chain.append(self.ffmpeg_normalize.post_filter)
|
|
841
|
+
|
|
842
|
+
return filter_chain
|
|
843
|
+
|
|
771
844
|
def _get_audio_filter_cmd(self) -> tuple[str, list[str]]:
|
|
772
845
|
"""
|
|
773
846
|
Return the audio filter command and output labels needed.
|
|
@@ -778,76 +851,54 @@ class MediaFile:
|
|
|
778
851
|
filter_chains = []
|
|
779
852
|
output_labels = []
|
|
780
853
|
|
|
781
|
-
|
|
854
|
+
self._stream_filter_chains = {}
|
|
782
855
|
|
|
783
|
-
for audio_stream in
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if self.ffmpeg_normalize.normalization_type == "ebu":
|
|
787
|
-
if (
|
|
788
|
-
audio_stream.loudness_statistics["ebu_pass1"] is not None
|
|
789
|
-
and audio_stream.loudness_statistics["ebu_pass1"]["input_i"]
|
|
790
|
-
< self.ffmpeg_normalize.target_level
|
|
791
|
-
):
|
|
792
|
-
skip_normalization = True
|
|
793
|
-
elif self.ffmpeg_normalize.normalization_type == "peak":
|
|
794
|
-
if (
|
|
795
|
-
audio_stream.loudness_statistics["max"] is not None
|
|
796
|
-
and audio_stream.loudness_statistics["max"]
|
|
797
|
-
< self.ffmpeg_normalize.target_level
|
|
798
|
-
):
|
|
799
|
-
skip_normalization = True
|
|
800
|
-
elif self.ffmpeg_normalize.normalization_type == "rms":
|
|
801
|
-
if (
|
|
802
|
-
audio_stream.loudness_statistics["mean"] is not None
|
|
803
|
-
and audio_stream.loudness_statistics["mean"]
|
|
804
|
-
< self.ffmpeg_normalize.target_level
|
|
805
|
-
):
|
|
806
|
-
skip_normalization = True
|
|
807
|
-
|
|
808
|
-
if skip_normalization:
|
|
809
|
-
_logger.warning(
|
|
810
|
-
f"Stream {audio_stream.stream_id} had measured input loudness lower than target, skipping normalization."
|
|
811
|
-
)
|
|
812
|
-
normalization_filter = "acopy"
|
|
813
|
-
else:
|
|
814
|
-
if self.ffmpeg_normalize.normalization_type == "ebu":
|
|
815
|
-
normalization_filter = audio_stream.get_second_pass_opts_ebu(
|
|
816
|
-
batch_reference=self.batch_reference
|
|
817
|
-
)
|
|
818
|
-
else:
|
|
819
|
-
normalization_filter = audio_stream.get_second_pass_opts_peakrms(
|
|
820
|
-
batch_reference=self.batch_reference
|
|
821
|
-
)
|
|
856
|
+
for audio_stream in self._get_streams_to_normalize():
|
|
857
|
+
filter_chain = self._get_stream_filter_chain(audio_stream)
|
|
858
|
+
self._stream_filter_chains[audio_stream.stream_id] = filter_chain
|
|
822
859
|
|
|
823
860
|
input_label = f"[0:{audio_stream.stream_id}]"
|
|
824
861
|
output_label = f"[norm{audio_stream.stream_id}]"
|
|
825
862
|
output_labels.append(output_label)
|
|
826
863
|
|
|
827
|
-
filter_chain = []
|
|
828
|
-
|
|
829
|
-
if self.ffmpeg_normalize.pre_filter:
|
|
830
|
-
filter_chain.append(self.ffmpeg_normalize.pre_filter)
|
|
831
|
-
|
|
832
|
-
# Apply channel conversion before normalization so that the
|
|
833
|
-
# normalization filter operates on the same channel layout that
|
|
834
|
-
# was measured in the first pass. See issue #316.
|
|
835
|
-
if self.ffmpeg_normalize.audio_channels:
|
|
836
|
-
filter_chain.append(
|
|
837
|
-
f"aformat=sample_fmts=fltp:channel_layouts={self.ffmpeg_normalize.audio_channels}c"
|
|
838
|
-
)
|
|
839
|
-
|
|
840
|
-
filter_chain.append(normalization_filter)
|
|
841
|
-
|
|
842
|
-
if self.ffmpeg_normalize.post_filter:
|
|
843
|
-
filter_chain.append(self.ffmpeg_normalize.post_filter)
|
|
844
|
-
|
|
845
864
|
filter_chains.append(input_label + ",".join(filter_chain) + output_label)
|
|
846
865
|
|
|
847
866
|
filter_complex_cmd = ";".join(filter_chains)
|
|
848
867
|
|
|
849
868
|
return filter_complex_cmd, output_labels
|
|
850
869
|
|
|
870
|
+
def _get_encoder_settings(
|
|
871
|
+
self, audio_stream: AudioStream, audio_codec: str | None
|
|
872
|
+
) -> str:
|
|
873
|
+
"""
|
|
874
|
+
Return an ffmpeg-command-equivalent description of how a single audio
|
|
875
|
+
stream is encoded in the second pass: the codec, then the bitrate,
|
|
876
|
+
sample rate and channel count when set, followed by the effective
|
|
877
|
+
filter chain (which includes the loudnorm measured values).
|
|
878
|
+
|
|
879
|
+
Written as the ``ENCODER_SETTINGS`` stream tag when
|
|
880
|
+
``write_encoder_settings`` is enabled. The filter chain comes from the
|
|
881
|
+
cache populated by :meth:`_get_audio_filter_cmd`, which must run first.
|
|
882
|
+
The loudnorm ``print_format`` option is dropped, as it only controls
|
|
883
|
+
first-pass diagnostics and not the normalization itself.
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
str: e.g. ``-c:a libopus -b:a 128000 -af loudnorm=i=-23.0:...``
|
|
887
|
+
"""
|
|
888
|
+
codec = audio_codec if audio_codec else audio_stream.get_pcm_codec()
|
|
889
|
+
args = ["-c:a", codec]
|
|
890
|
+
if self.ffmpeg_normalize.audio_bitrate:
|
|
891
|
+
args += ["-b:a", str(self.ffmpeg_normalize.audio_bitrate)]
|
|
892
|
+
if self.ffmpeg_normalize.sample_rate:
|
|
893
|
+
args += ["-ar", str(self.ffmpeg_normalize.sample_rate)]
|
|
894
|
+
if self.ffmpeg_normalize.audio_channels:
|
|
895
|
+
args += ["-ac", str(self.ffmpeg_normalize.audio_channels)]
|
|
896
|
+
filter_chain = self._stream_filter_chains.get(audio_stream.stream_id, [])
|
|
897
|
+
if filter_chain:
|
|
898
|
+
af = re.sub(r":print_format=\w+", "", ",".join(filter_chain))
|
|
899
|
+
args += ["-af", af]
|
|
900
|
+
return shlex.join(args)
|
|
901
|
+
|
|
851
902
|
def _get_audio_codec(self) -> str | None:
|
|
852
903
|
"""
|
|
853
904
|
Return the audio encoder to use for normalized streams, or None to use
|
|
@@ -1011,6 +1062,21 @@ class MediaFile:
|
|
|
1011
1062
|
for idx in range(len(streams_to_normalize)):
|
|
1012
1063
|
cmd.extend([f"-ac:a:{idx}", str(self.ffmpeg_normalize.audio_channels)])
|
|
1013
1064
|
|
|
1065
|
+
# record how each normalized stream was encoded as an ENCODER_SETTINGS
|
|
1066
|
+
# tag (a Matroska/WebM convention), so the output documents itself
|
|
1067
|
+
if (
|
|
1068
|
+
self.ffmpeg_normalize.write_encoder_settings
|
|
1069
|
+
and not self.ffmpeg_normalize.metadata_disable
|
|
1070
|
+
):
|
|
1071
|
+
for idx, audio_stream in enumerate(streams_to_normalize):
|
|
1072
|
+
cmd.extend(
|
|
1073
|
+
[
|
|
1074
|
+
f"-metadata:s:a:{idx}",
|
|
1075
|
+
"ENCODER_SETTINGS="
|
|
1076
|
+
+ self._get_encoder_settings(audio_stream, audio_codec),
|
|
1077
|
+
]
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1014
1080
|
# carry the input bit depth through to the output encoder, if requested
|
|
1015
1081
|
if self.ffmpeg_normalize.keep_bit_depth:
|
|
1016
1082
|
for idx, audio_stream in enumerate(streams_to_normalize):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/data/presets/music.json
RENAMED
|
File without changes
|
{ffmpeg_normalize-1.41.1 → ffmpeg_normalize-1.42.1}/src/ffmpeg_normalize/data/presets/podcast.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|