ffmpeg-normalize 1.27.7__tar.gz → 1.28.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.27.7 → ffmpeg-normalize-1.28.1}/CHANGELOG.md +12 -0
- {ffmpeg-normalize-1.27.7/ffmpeg_normalize.egg-info → ffmpeg-normalize-1.28.1}/PKG-INFO +15 -2
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/README.md +2 -1
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/__main__.py +2 -4
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_cmd_utils.py +4 -3
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_media_file.py +37 -9
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_streams.py +89 -24
- ffmpeg-normalize-1.28.1/ffmpeg_normalize/_version.py +1 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1/ffmpeg_normalize.egg-info}/PKG-INFO +15 -2
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/test/test.py +21 -8
- ffmpeg-normalize-1.27.7/ffmpeg_normalize/_version.py +0 -1
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/LICENSE +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/__init__.py +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_errors.py +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_ffmpeg_normalize.py +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_logger.py +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/py.typed +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/SOURCES.txt +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/dependency_links.txt +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/entry_points.txt +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/not-zip-safe +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/requires.txt +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/top_level.txt +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/setup.cfg +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/setup.py +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/test/out.mp4 +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/test/test.mp4 +0 -0
- {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/test/test.wav +0 -0
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v1.28.1 (2024-05-15)
|
|
5
|
+
|
|
6
|
+
* Fix assignment of audio statistics, fixes #257.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## v1.28.0 (2024-05-13)
|
|
10
|
+
|
|
11
|
+
* Warn if dynamic mode is used but linear specified (#256)
|
|
12
|
+
|
|
13
|
+
* Print debug commands in shell-escaped form.
|
|
14
|
+
|
|
15
|
+
|
|
4
16
|
## v1.27.7 (2023-09-26)
|
|
5
17
|
|
|
6
18
|
* Allow cover art in MP3.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ffmpeg-normalize
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.28.1
|
|
4
4
|
Summary: Normalize audio via ffmpeg
|
|
5
5
|
Home-page: https://github.com/slhck/ffmpeg-normalize
|
|
6
6
|
Author: Werner Robitza
|
|
@@ -53,6 +53,7 @@ Read on for more info.
|
|
|
53
53
|
- [Requirements](#requirements)
|
|
54
54
|
- [ffmpeg](#ffmpeg)
|
|
55
55
|
- [Installation](#installation)
|
|
56
|
+
- [Docker Build](#docker-build)
|
|
56
57
|
- [Usage](#usage)
|
|
57
58
|
- [Description](#description)
|
|
58
59
|
- [Examples](#examples)
|
|
@@ -262,7 +263,7 @@ Some containers (like MP4) also cannot handle PCM audio. If you want to use such
|
|
|
262
263
|
|
|
263
264
|
Otherwise, the range is -99 to 0.
|
|
264
265
|
|
|
265
|
-
- `-p, --print-stats`: Print
|
|
266
|
+
- `-p, --print-stats`: Print loudness statistics for both passes formatted as JSON to stdout.
|
|
266
267
|
|
|
267
268
|
### EBU R128 Normalization
|
|
268
269
|
|
|
@@ -558,6 +559,18 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
558
559
|
# Changelog
|
|
559
560
|
|
|
560
561
|
|
|
562
|
+
## v1.28.1 (2024-05-15)
|
|
563
|
+
|
|
564
|
+
* Fix assignment of audio statistics, fixes #257.
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
## v1.28.0 (2024-05-13)
|
|
568
|
+
|
|
569
|
+
* Warn if dynamic mode is used but linear specified (#256)
|
|
570
|
+
|
|
571
|
+
* Print debug commands in shell-escaped form.
|
|
572
|
+
|
|
573
|
+
|
|
561
574
|
## v1.27.7 (2023-09-26)
|
|
562
575
|
|
|
563
576
|
* Allow cover art in MP3.
|
|
@@ -28,6 +28,7 @@ Read on for more info.
|
|
|
28
28
|
- [Requirements](#requirements)
|
|
29
29
|
- [ffmpeg](#ffmpeg)
|
|
30
30
|
- [Installation](#installation)
|
|
31
|
+
- [Docker Build](#docker-build)
|
|
31
32
|
- [Usage](#usage)
|
|
32
33
|
- [Description](#description)
|
|
33
34
|
- [Examples](#examples)
|
|
@@ -237,7 +238,7 @@ Some containers (like MP4) also cannot handle PCM audio. If you want to use such
|
|
|
237
238
|
|
|
238
239
|
Otherwise, the range is -99 to 0.
|
|
239
240
|
|
|
240
|
-
- `-p, --print-stats`: Print
|
|
241
|
+
- `-p, --print-stats`: Print loudness statistics for both passes formatted as JSON to stdout.
|
|
241
242
|
|
|
242
243
|
### EBU R128 Normalization
|
|
243
244
|
|
|
@@ -24,9 +24,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
24
24
|
description=textwrap.dedent(
|
|
25
25
|
"""\
|
|
26
26
|
ffmpeg-normalize v{} -- command line tool for normalizing audio files
|
|
27
|
-
""".format(
|
|
28
|
-
__version__
|
|
29
|
-
)
|
|
27
|
+
""".format(__version__)
|
|
30
28
|
),
|
|
31
29
|
# usage="%(prog)s INPUT [INPUT ...] [-o OUTPUT [OUTPUT ...]] [options]",
|
|
32
30
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
@@ -157,7 +155,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
157
155
|
"-p",
|
|
158
156
|
"--print-stats",
|
|
159
157
|
action="store_true",
|
|
160
|
-
help="Print
|
|
158
|
+
help="Print loudness statistics for both passes formatted as JSON to stdout.",
|
|
161
159
|
)
|
|
162
160
|
|
|
163
161
|
# group_normalization.add_argument(
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
|
+
import shlex
|
|
6
7
|
import subprocess
|
|
7
8
|
from platform import system
|
|
8
9
|
from shutil import which
|
|
@@ -75,7 +76,7 @@ class CommandRunner:
|
|
|
75
76
|
float: Progress percentage
|
|
76
77
|
"""
|
|
77
78
|
# wrapper for 'ffmpeg-progress-yield'
|
|
78
|
-
_logger.debug(f"Running command: {cmd}")
|
|
79
|
+
_logger.debug(f"Running command: {shlex.join(cmd)}")
|
|
79
80
|
ff = FfmpegProgress(cmd, dry_run=self.dry)
|
|
80
81
|
yield from ff.run_command_with_progress()
|
|
81
82
|
|
|
@@ -96,7 +97,7 @@ class CommandRunner:
|
|
|
96
97
|
Raises:
|
|
97
98
|
RuntimeError: If command returns non-zero exit code
|
|
98
99
|
"""
|
|
99
|
-
_logger.debug(f"Running command: {cmd}")
|
|
100
|
+
_logger.debug(f"Running command: {shlex.join(cmd)}")
|
|
100
101
|
|
|
101
102
|
if self.dry:
|
|
102
103
|
_logger.debug("Dry mode specified, not actually running command")
|
|
@@ -116,7 +117,7 @@ class CommandRunner:
|
|
|
116
117
|
stderr = stderr_bytes.decode("utf8", errors="replace")
|
|
117
118
|
|
|
118
119
|
if p.returncode != 0:
|
|
119
|
-
raise RuntimeError(f"Error running command {cmd}: {stderr}")
|
|
120
|
+
raise RuntimeError(f"Error running command {shlex.join(cmd)}: {stderr}")
|
|
120
121
|
|
|
121
122
|
self.output = stdout + stderr
|
|
122
123
|
return self
|
|
@@ -232,12 +232,10 @@ class MediaFile:
|
|
|
232
232
|
for _ in fun():
|
|
233
233
|
pass
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
]
|
|
240
|
-
self.ffmpeg_normalize.stats.extend(stats)
|
|
235
|
+
# set initial stats (for dry-runs, this is the only thing we need to do)
|
|
236
|
+
self.ffmpeg_normalize.stats = [
|
|
237
|
+
audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
|
|
238
|
+
]
|
|
241
239
|
|
|
242
240
|
def _get_audio_filter_cmd(self) -> tuple[str, list[str]]:
|
|
243
241
|
"""
|
|
@@ -390,12 +388,14 @@ class MediaFile:
|
|
|
390
388
|
temp_file = os.path.join(temp_dir, f"out.{self.output_ext}")
|
|
391
389
|
cmd.append(temp_file)
|
|
392
390
|
|
|
391
|
+
cmd_runner = CommandRunner()
|
|
393
392
|
try:
|
|
394
393
|
try:
|
|
395
|
-
yield from
|
|
394
|
+
yield from cmd_runner.run_ffmpeg_command(cmd)
|
|
396
395
|
except Exception as e:
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
_logger.error(
|
|
397
|
+
f"Error while running command {shlex.join(cmd)}! Error: {e}"
|
|
398
|
+
)
|
|
399
399
|
raise e
|
|
400
400
|
else:
|
|
401
401
|
_logger.debug(
|
|
@@ -407,4 +407,32 @@ class MediaFile:
|
|
|
407
407
|
rmtree(temp_dir, ignore_errors=True)
|
|
408
408
|
raise e
|
|
409
409
|
|
|
410
|
+
output = cmd_runner.get_output()
|
|
411
|
+
# in the second pass, we do not normalize stream-by-stream, so we set the stats based on the
|
|
412
|
+
# overall output (which includes multiple loudnorm stats)
|
|
413
|
+
if self.ffmpeg_normalize.normalization_type == "ebu":
|
|
414
|
+
all_stats = AudioStream.prune_and_parse_loudnorm_output(
|
|
415
|
+
output, num_stats=len(self.streams["audio"])
|
|
416
|
+
)
|
|
417
|
+
for idx, audio_stream in enumerate(self.streams["audio"].values()):
|
|
418
|
+
audio_stream.set_second_pass_stats(all_stats[idx])
|
|
419
|
+
|
|
420
|
+
# collect all stats for the final report, again (overwrite the input)
|
|
421
|
+
self.ffmpeg_normalize.stats = [
|
|
422
|
+
audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
# warn if self.media_file.ffmpeg_normalize.dynamic == False and any of the second pass stats contain "normalization_type" == "dynamic"
|
|
426
|
+
if self.ffmpeg_normalize.dynamic is False:
|
|
427
|
+
for audio_stream in self.streams["audio"].values():
|
|
428
|
+
pass2_stats = audio_stream.get_stats()["ebu_pass2"]
|
|
429
|
+
if pass2_stats is None:
|
|
430
|
+
continue
|
|
431
|
+
if pass2_stats["normalization_type"] == "dynamic":
|
|
432
|
+
_logger.warning(
|
|
433
|
+
"You specified linear normalization, but the loudnorm filter reverted to dynamic normalization. "
|
|
434
|
+
"This may lead to unexpected results."
|
|
435
|
+
"Consider your input settings, e.g. choose a lower target level or higher target loudness range."
|
|
436
|
+
)
|
|
437
|
+
|
|
410
438
|
_logger.debug("Normalization finished")
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
-
from typing import TYPE_CHECKING, Iterator, Literal, TypedDict, cast
|
|
7
|
+
from typing import TYPE_CHECKING, Iterator, List, Literal, Optional, TypedDict, cast
|
|
8
8
|
|
|
9
9
|
from ._cmd_utils import NUL, CommandRunner, dict_to_filter_opts
|
|
10
10
|
from ._errors import FFmpegNormalizeError
|
|
@@ -26,10 +26,12 @@ class EbuLoudnessStatistics(TypedDict):
|
|
|
26
26
|
output_lra: float
|
|
27
27
|
output_thresh: float
|
|
28
28
|
target_offset: float
|
|
29
|
+
normalization_type: str
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class LoudnessStatistics(TypedDict):
|
|
32
|
-
|
|
33
|
+
ebu_pass1: EbuLoudnessStatistics | None
|
|
34
|
+
ebu_pass2: EbuLoudnessStatistics | None
|
|
33
35
|
mean: float | None
|
|
34
36
|
max: float | None
|
|
35
37
|
|
|
@@ -107,7 +109,8 @@ class AudioStream(MediaStream):
|
|
|
107
109
|
super().__init__(ffmpeg_normalize, media_file, "audio", stream_id)
|
|
108
110
|
|
|
109
111
|
self.loudness_statistics: LoudnessStatistics = {
|
|
110
|
-
"
|
|
112
|
+
"ebu_pass1": None,
|
|
113
|
+
"ebu_pass2": None,
|
|
111
114
|
"mean": None,
|
|
112
115
|
"max": None,
|
|
113
116
|
}
|
|
@@ -156,12 +159,22 @@ class AudioStream(MediaStream):
|
|
|
156
159
|
"input_file": self.media_file.input_file,
|
|
157
160
|
"output_file": self.media_file.output_file,
|
|
158
161
|
"stream_id": self.stream_id,
|
|
159
|
-
"
|
|
162
|
+
"ebu_pass1": self.loudness_statistics["ebu_pass1"],
|
|
163
|
+
"ebu_pass2": self.loudness_statistics["ebu_pass2"],
|
|
160
164
|
"mean": self.loudness_statistics["mean"],
|
|
161
165
|
"max": self.loudness_statistics["max"],
|
|
162
166
|
}
|
|
163
167
|
return stats
|
|
164
168
|
|
|
169
|
+
def set_second_pass_stats(self, stats: EbuLoudnessStatistics):
|
|
170
|
+
"""
|
|
171
|
+
Set the EBU loudness statistics for the second pass.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
stats (dict): The EBU loudness statistics.
|
|
175
|
+
"""
|
|
176
|
+
self.loudness_statistics["ebu_pass2"] = stats
|
|
177
|
+
|
|
165
178
|
def get_pcm_codec(self) -> str:
|
|
166
179
|
"""
|
|
167
180
|
Get the PCM codec string for the stream.
|
|
@@ -288,6 +301,8 @@ class AudioStream(MediaStream):
|
|
|
288
301
|
"-y",
|
|
289
302
|
"-i",
|
|
290
303
|
self.media_file.input_file,
|
|
304
|
+
"-map",
|
|
305
|
+
f"0:{self.stream_id}",
|
|
291
306
|
"-filter_complex",
|
|
292
307
|
filter_str,
|
|
293
308
|
"-vn",
|
|
@@ -305,30 +320,69 @@ class AudioStream(MediaStream):
|
|
|
305
320
|
f"Loudnorm first pass command output: {CommandRunner.prune_ffmpeg_progress_from_output(output)}"
|
|
306
321
|
)
|
|
307
322
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
323
|
+
self.loudness_statistics["ebu_pass1"] = (
|
|
324
|
+
AudioStream.prune_and_parse_loudnorm_output(
|
|
325
|
+
output, num_stats=1
|
|
326
|
+
)[0] # only one stream
|
|
312
327
|
)
|
|
313
328
|
|
|
314
329
|
@staticmethod
|
|
315
|
-
def
|
|
330
|
+
def prune_and_parse_loudnorm_output(
|
|
331
|
+
output: str, num_stats: int = 1
|
|
332
|
+
) -> List[EbuLoudnessStatistics]:
|
|
333
|
+
"""
|
|
334
|
+
Prune ffmpeg progress lines from output and parse the loudnorm filter output.
|
|
335
|
+
There may be multiple outputs if multiple streams were processed.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
output (str): The output from ffmpeg.
|
|
339
|
+
num_stats (int): The number of loudnorm statistics to parse.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
list: The EBU loudness statistics.
|
|
343
|
+
"""
|
|
344
|
+
pruned_output = CommandRunner.prune_ffmpeg_progress_from_output(output)
|
|
345
|
+
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
|
|
364
|
+
|
|
365
|
+
@staticmethod
|
|
366
|
+
def _parse_loudnorm_output(
|
|
367
|
+
output_lines: list[str], stream_index: Optional[int] = None
|
|
368
|
+
) -> Optional[EbuLoudnessStatistics]:
|
|
316
369
|
"""
|
|
317
370
|
Parse the output of a loudnorm filter to get the EBU loudness statistics.
|
|
318
371
|
|
|
319
372
|
Args:
|
|
320
373
|
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.
|
|
321
375
|
|
|
322
376
|
Raises:
|
|
323
377
|
FFmpegNormalizeError: When the output could not be parsed.
|
|
324
378
|
|
|
325
379
|
Returns:
|
|
326
|
-
EbuLoudnessStatistics: The EBU loudness statistics.
|
|
380
|
+
EbuLoudnessStatistics: The EBU loudness statistics, if found.
|
|
327
381
|
"""
|
|
328
382
|
loudnorm_start = 0
|
|
329
383
|
loudnorm_end = 0
|
|
330
384
|
for index, line in enumerate(output_lines):
|
|
331
|
-
if line.startswith("[
|
|
385
|
+
if line.startswith(f"[Parsed_loudnorm_{stream_index}"):
|
|
332
386
|
loudnorm_start = index + 1
|
|
333
387
|
continue
|
|
334
388
|
if loudnorm_start and line.startswith("}"):
|
|
@@ -336,6 +390,10 @@ class AudioStream(MediaStream):
|
|
|
336
390
|
break
|
|
337
391
|
|
|
338
392
|
if not (loudnorm_start and loudnorm_end):
|
|
393
|
+
if stream_index is not None:
|
|
394
|
+
# not an error
|
|
395
|
+
return None
|
|
396
|
+
|
|
339
397
|
raise FFmpegNormalizeError(
|
|
340
398
|
"Could not parse loudnorm stats; no loudnorm-related output found"
|
|
341
399
|
)
|
|
@@ -345,7 +403,9 @@ class AudioStream(MediaStream):
|
|
|
345
403
|
"\n".join(output_lines[loudnorm_start:loudnorm_end])
|
|
346
404
|
)
|
|
347
405
|
|
|
348
|
-
_logger.debug(
|
|
406
|
+
_logger.debug(
|
|
407
|
+
f"Loudnorm stats for stream {stream_index} parsed: {json.dumps(loudnorm_stats)}"
|
|
408
|
+
)
|
|
349
409
|
|
|
350
410
|
for key in [
|
|
351
411
|
"input_i",
|
|
@@ -357,9 +417,14 @@ class AudioStream(MediaStream):
|
|
|
357
417
|
"output_lra",
|
|
358
418
|
"output_thresh",
|
|
359
419
|
"target_offset",
|
|
420
|
+
"normalization_type",
|
|
360
421
|
]:
|
|
422
|
+
if key not in loudnorm_stats:
|
|
423
|
+
continue
|
|
424
|
+
if key == "normalization_type":
|
|
425
|
+
loudnorm_stats[key] = loudnorm_stats[key].lower()
|
|
361
426
|
# handle infinite values
|
|
362
|
-
|
|
427
|
+
elif float(loudnorm_stats[key]) == -float("inf"):
|
|
363
428
|
loudnorm_stats[key] = -99
|
|
364
429
|
elif float(loudnorm_stats[key]) == float("inf"):
|
|
365
430
|
loudnorm_stats[key] = 0
|
|
@@ -378,17 +443,17 @@ class AudioStream(MediaStream):
|
|
|
378
443
|
Return second pass loudnorm filter options string for ffmpeg
|
|
379
444
|
"""
|
|
380
445
|
|
|
381
|
-
if not self.loudness_statistics["
|
|
446
|
+
if not self.loudness_statistics["ebu_pass1"]:
|
|
382
447
|
raise FFmpegNormalizeError(
|
|
383
448
|
"First pass not run, you must call parse_loudnorm_stats first"
|
|
384
449
|
)
|
|
385
450
|
|
|
386
|
-
if float(self.loudness_statistics["
|
|
451
|
+
if float(self.loudness_statistics["ebu_pass1"]["input_i"]) > 0:
|
|
387
452
|
_logger.warning(
|
|
388
453
|
"Input file had measured input loudness greater than zero "
|
|
389
|
-
f"({self.loudness_statistics['
|
|
454
|
+
f"({self.loudness_statistics['ebu_pass1']['input_i']}), capping at 0"
|
|
390
455
|
)
|
|
391
|
-
self.loudness_statistics["
|
|
456
|
+
self.loudness_statistics["ebu_pass1"]["input_i"] = 0
|
|
392
457
|
|
|
393
458
|
will_use_dynamic_mode = self.media_file.ffmpeg_normalize.dynamic
|
|
394
459
|
|
|
@@ -396,7 +461,7 @@ class AudioStream(MediaStream):
|
|
|
396
461
|
_logger.debug(
|
|
397
462
|
"Keeping target loudness range in second pass loudnorm filter"
|
|
398
463
|
)
|
|
399
|
-
input_lra = self.loudness_statistics["
|
|
464
|
+
input_lra = self.loudness_statistics["ebu_pass1"]["input_lra"]
|
|
400
465
|
if input_lra < 1 or input_lra > 50:
|
|
401
466
|
_logger.warning(
|
|
402
467
|
"Input file had measured loudness range outside of [1,50] "
|
|
@@ -404,12 +469,12 @@ class AudioStream(MediaStream):
|
|
|
404
469
|
)
|
|
405
470
|
|
|
406
471
|
self.media_file.ffmpeg_normalize.loudness_range_target = self._constrain(
|
|
407
|
-
self.loudness_statistics["
|
|
472
|
+
self.loudness_statistics["ebu_pass1"]["input_lra"], 1, 50
|
|
408
473
|
)
|
|
409
474
|
|
|
410
475
|
if self.media_file.ffmpeg_normalize.keep_lra_above_loudness_range_target:
|
|
411
476
|
if (
|
|
412
|
-
self.loudness_statistics["
|
|
477
|
+
self.loudness_statistics["ebu_pass1"]["input_lra"]
|
|
413
478
|
<= self.media_file.ffmpeg_normalize.loudness_range_target
|
|
414
479
|
):
|
|
415
480
|
_logger.debug(
|
|
@@ -417,7 +482,7 @@ class AudioStream(MediaStream):
|
|
|
417
482
|
)
|
|
418
483
|
else:
|
|
419
484
|
self.media_file.ffmpeg_normalize.loudness_range_target = (
|
|
420
|
-
self.loudness_statistics["
|
|
485
|
+
self.loudness_statistics["ebu_pass1"]["input_lra"]
|
|
421
486
|
)
|
|
422
487
|
_logger.debug(
|
|
423
488
|
"Keeping target loudness range in second pass loudnorm filter"
|
|
@@ -425,11 +490,11 @@ class AudioStream(MediaStream):
|
|
|
425
490
|
|
|
426
491
|
if (
|
|
427
492
|
self.media_file.ffmpeg_normalize.loudness_range_target
|
|
428
|
-
< self.loudness_statistics["
|
|
493
|
+
< self.loudness_statistics["ebu_pass1"]["input_lra"]
|
|
429
494
|
and not will_use_dynamic_mode
|
|
430
495
|
):
|
|
431
496
|
_logger.warning(
|
|
432
|
-
f"Input file had loudness range of {self.loudness_statistics['
|
|
497
|
+
f"Input file had loudness range of {self.loudness_statistics['ebu_pass1']['input_lra']}. "
|
|
433
498
|
f"This is larger than the loudness range target ({self.media_file.ffmpeg_normalize.loudness_range_target}). "
|
|
434
499
|
"Normalization will revert to dynamic mode. Choose a higher target loudness range if you want linear normalization. "
|
|
435
500
|
"Alternatively, use the --keep-loudness-range-target or --keep-lra-above-loudness-range-target option to keep the target loudness range from "
|
|
@@ -443,7 +508,7 @@ class AudioStream(MediaStream):
|
|
|
443
508
|
"Specify -ar/--sample-rate to override it."
|
|
444
509
|
)
|
|
445
510
|
|
|
446
|
-
stats = self.loudness_statistics["
|
|
511
|
+
stats = self.loudness_statistics["ebu_pass1"]
|
|
447
512
|
|
|
448
513
|
opts = {
|
|
449
514
|
"i": self.media_file.ffmpeg_normalize.target_level,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.28.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ffmpeg-normalize
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.28.1
|
|
4
4
|
Summary: Normalize audio via ffmpeg
|
|
5
5
|
Home-page: https://github.com/slhck/ffmpeg-normalize
|
|
6
6
|
Author: Werner Robitza
|
|
@@ -53,6 +53,7 @@ Read on for more info.
|
|
|
53
53
|
- [Requirements](#requirements)
|
|
54
54
|
- [ffmpeg](#ffmpeg)
|
|
55
55
|
- [Installation](#installation)
|
|
56
|
+
- [Docker Build](#docker-build)
|
|
56
57
|
- [Usage](#usage)
|
|
57
58
|
- [Description](#description)
|
|
58
59
|
- [Examples](#examples)
|
|
@@ -262,7 +263,7 @@ Some containers (like MP4) also cannot handle PCM audio. If you want to use such
|
|
|
262
263
|
|
|
263
264
|
Otherwise, the range is -99 to 0.
|
|
264
265
|
|
|
265
|
-
- `-p, --print-stats`: Print
|
|
266
|
+
- `-p, --print-stats`: Print loudness statistics for both passes formatted as JSON to stdout.
|
|
266
267
|
|
|
267
268
|
### EBU R128 Normalization
|
|
268
269
|
|
|
@@ -558,6 +559,18 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
558
559
|
# Changelog
|
|
559
560
|
|
|
560
561
|
|
|
562
|
+
## v1.28.1 (2024-05-15)
|
|
563
|
+
|
|
564
|
+
* Fix assignment of audio statistics, fixes #257.
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
## v1.28.0 (2024-05-13)
|
|
568
|
+
|
|
569
|
+
* Warn if dynamic mode is used but linear specified (#256)
|
|
570
|
+
|
|
571
|
+
* Print debug commands in shell-escaped form.
|
|
572
|
+
|
|
573
|
+
|
|
561
574
|
## v1.27.7 (2023-09-26)
|
|
562
575
|
|
|
563
576
|
* Allow cover art in MP3.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import shlex
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
@@ -14,6 +15,7 @@ def ffmpeg_normalize_call(args: List[str]) -> Tuple[str, str]:
|
|
|
14
15
|
cmd = [sys.executable, "-m", "ffmpeg_normalize"]
|
|
15
16
|
cmd.extend(args)
|
|
16
17
|
|
|
18
|
+
print(shlex.join(cmd))
|
|
17
19
|
try:
|
|
18
20
|
p = subprocess.Popen(
|
|
19
21
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
|
|
@@ -28,6 +30,9 @@ def ffmpeg_normalize_call(args: List[str]) -> Tuple[str, str]:
|
|
|
28
30
|
def _get_stats(
|
|
29
31
|
input_file: str, normalization_type: Literal["ebu", "rms", "peak"] = "ebu"
|
|
30
32
|
) -> Dict:
|
|
33
|
+
"""
|
|
34
|
+
Get the statistics from an existing output file without converting it.
|
|
35
|
+
"""
|
|
31
36
|
stdout, _ = ffmpeg_normalize_call(
|
|
32
37
|
[input_file, "-f", "-n", "--print-stats", "-nt", normalization_type]
|
|
33
38
|
)
|
|
@@ -192,7 +197,8 @@ class TestFFmpegNormalize:
|
|
|
192
197
|
"input_file": "normalized/test.mkv",
|
|
193
198
|
"output_file": "normalized/test.mkv",
|
|
194
199
|
"stream_id": 1,
|
|
195
|
-
"
|
|
200
|
+
"ebu_pass1": None,
|
|
201
|
+
"ebu_pass2": None,
|
|
196
202
|
"mean": -14.8,
|
|
197
203
|
"max": -0.0,
|
|
198
204
|
},
|
|
@@ -200,7 +206,8 @@ class TestFFmpegNormalize:
|
|
|
200
206
|
"input_file": "normalized/test.mkv",
|
|
201
207
|
"output_file": "normalized/test.mkv",
|
|
202
208
|
"stream_id": 2,
|
|
203
|
-
"
|
|
209
|
+
"ebu_pass1": None,
|
|
210
|
+
"ebu_pass2": None,
|
|
204
211
|
"mean": -19.3,
|
|
205
212
|
"max": -0.0,
|
|
206
213
|
},
|
|
@@ -217,7 +224,8 @@ class TestFFmpegNormalize:
|
|
|
217
224
|
"input_file": "normalized/test.mkv",
|
|
218
225
|
"output_file": "normalized/test.mkv",
|
|
219
226
|
"stream_id": 1,
|
|
220
|
-
"
|
|
227
|
+
"ebu_pass1": None,
|
|
228
|
+
"ebu_pass2": None,
|
|
221
229
|
"mean": -15.0,
|
|
222
230
|
"max": -0.2,
|
|
223
231
|
},
|
|
@@ -225,7 +233,8 @@ class TestFFmpegNormalize:
|
|
|
225
233
|
"input_file": "normalized/test.mkv",
|
|
226
234
|
"output_file": "normalized/test.mkv",
|
|
227
235
|
"stream_id": 2,
|
|
228
|
-
"
|
|
236
|
+
"ebu_pass1": None,
|
|
237
|
+
"ebu_pass2": None,
|
|
229
238
|
"mean": -15.1,
|
|
230
239
|
"max": 0.0,
|
|
231
240
|
},
|
|
@@ -242,7 +251,7 @@ class TestFFmpegNormalize:
|
|
|
242
251
|
"input_file": "normalized/test.mkv",
|
|
243
252
|
"output_file": "normalized/test.mkv",
|
|
244
253
|
"stream_id": 1,
|
|
245
|
-
"
|
|
254
|
+
"ebu_pass1": {
|
|
246
255
|
"input_i": -23.00,
|
|
247
256
|
"input_tp": -10.32,
|
|
248
257
|
"input_lra": 2.40,
|
|
@@ -254,6 +263,7 @@ class TestFFmpegNormalize:
|
|
|
254
263
|
"normalization_type": "dynamic",
|
|
255
264
|
"target_offset": -0.97,
|
|
256
265
|
},
|
|
266
|
+
"ebu_pass2": None,
|
|
257
267
|
"mean": None,
|
|
258
268
|
"max": None,
|
|
259
269
|
},
|
|
@@ -261,7 +271,7 @@ class TestFFmpegNormalize:
|
|
|
261
271
|
"input_file": "normalized/test.mkv",
|
|
262
272
|
"output_file": "normalized/test.mkv",
|
|
263
273
|
"stream_id": 2,
|
|
264
|
-
"
|
|
274
|
+
"ebu_pass1": {
|
|
265
275
|
"input_i": -22.98,
|
|
266
276
|
"input_tp": -10.72,
|
|
267
277
|
"input_lra": 2.10,
|
|
@@ -273,6 +283,7 @@ class TestFFmpegNormalize:
|
|
|
273
283
|
"normalization_type": "dynamic",
|
|
274
284
|
"target_offset": -0.84,
|
|
275
285
|
},
|
|
286
|
+
"ebu_pass2": None,
|
|
276
287
|
"mean": None,
|
|
277
288
|
"max": None,
|
|
278
289
|
},
|
|
@@ -388,7 +399,7 @@ class TestFFmpegNormalize:
|
|
|
388
399
|
"input_file": "normalized/test2.wav",
|
|
389
400
|
"output_file": "normalized/test2.mkv",
|
|
390
401
|
"stream_id": 0,
|
|
391
|
-
"
|
|
402
|
+
"ebu_pass1": {
|
|
392
403
|
"input_i": -23.01,
|
|
393
404
|
"input_tp": -10.75,
|
|
394
405
|
"input_lra": 2.20,
|
|
@@ -400,6 +411,7 @@ class TestFFmpegNormalize:
|
|
|
400
411
|
"normalization_type": "dynamic",
|
|
401
412
|
"target_offset": -0.84,
|
|
402
413
|
},
|
|
414
|
+
"ebu_pass2": None,
|
|
403
415
|
"mean": None,
|
|
404
416
|
"max": None,
|
|
405
417
|
}
|
|
@@ -424,7 +436,7 @@ class TestFFmpegNormalize:
|
|
|
424
436
|
"input_file": "normalized/test2.wav",
|
|
425
437
|
"output_file": "normalized/test2.mkv",
|
|
426
438
|
"stream_id": 0,
|
|
427
|
-
"
|
|
439
|
+
"ebu_pass1": {
|
|
428
440
|
"input_i": -35.02,
|
|
429
441
|
"input_tp": -22.76,
|
|
430
442
|
"input_lra": 2.20,
|
|
@@ -436,6 +448,7 @@ class TestFFmpegNormalize:
|
|
|
436
448
|
"normalization_type": "dynamic",
|
|
437
449
|
"target_offset": -0.84,
|
|
438
450
|
},
|
|
451
|
+
"ebu_pass2": None,
|
|
439
452
|
"mean": None,
|
|
440
453
|
"max": None,
|
|
441
454
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.27.7"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/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
|