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.
Files changed (28) hide show
  1. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/CHANGELOG.md +12 -0
  2. {ffmpeg-normalize-1.27.7/ffmpeg_normalize.egg-info → ffmpeg-normalize-1.28.1}/PKG-INFO +15 -2
  3. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/README.md +2 -1
  4. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/__main__.py +2 -4
  5. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_cmd_utils.py +4 -3
  6. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_media_file.py +37 -9
  7. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_streams.py +89 -24
  8. ffmpeg-normalize-1.28.1/ffmpeg_normalize/_version.py +1 -0
  9. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1/ffmpeg_normalize.egg-info}/PKG-INFO +15 -2
  10. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/test/test.py +21 -8
  11. ffmpeg-normalize-1.27.7/ffmpeg_normalize/_version.py +0 -1
  12. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/LICENSE +0 -0
  13. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/__init__.py +0 -0
  14. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_errors.py +0 -0
  15. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_ffmpeg_normalize.py +0 -0
  16. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/_logger.py +0 -0
  17. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize/py.typed +0 -0
  18. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/SOURCES.txt +0 -0
  19. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/dependency_links.txt +0 -0
  20. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/entry_points.txt +0 -0
  21. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/not-zip-safe +0 -0
  22. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/requires.txt +0 -0
  23. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/ffmpeg_normalize.egg-info/top_level.txt +0 -0
  24. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/setup.cfg +0 -0
  25. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/setup.py +0 -0
  26. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/test/out.mp4 +0 -0
  27. {ffmpeg-normalize-1.27.7 → ffmpeg-normalize-1.28.1}/test/test.mp4 +0 -0
  28. {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.27.7
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 first pass loudness statistics formatted as JSON to stdout.
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 first pass loudness statistics formatted as JSON to stdout.
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 first pass loudness statistics formatted as JSON to stdout",
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
- if self.ffmpeg_normalize.print_stats:
236
- stats = [
237
- audio_stream.get_stats()
238
- for audio_stream in self.streams["audio"].values()
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 CommandRunner().run_ffmpeg_command(cmd)
394
+ yield from cmd_runner.run_ffmpeg_command(cmd)
396
395
  except Exception as e:
397
- cmd_str = " ".join([shlex.quote(c) for c in cmd])
398
- _logger.error(f"Error while running command {cmd_str}! Error: {e}")
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
- ebu: EbuLoudnessStatistics | None
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
- "ebu": None,
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
- "ebu": self.loudness_statistics["ebu"],
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
- output_lines = [line.strip() for line in output.split("\n")]
309
-
310
- self.loudness_statistics["ebu"] = AudioStream._parse_loudnorm_output(
311
- output_lines
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 _parse_loudnorm_output(output_lines: list[str]) -> EbuLoudnessStatistics:
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("[Parsed_loudnorm"):
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(f"Loudnorm stats parsed: {json.dumps(loudnorm_stats)}")
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
- if float(loudnorm_stats[key]) == -float("inf"):
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["ebu"]:
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["ebu"]["input_i"]) > 0:
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['ebu']['input_i']}), capping at 0"
454
+ f"({self.loudness_statistics['ebu_pass1']['input_i']}), capping at 0"
390
455
  )
391
- self.loudness_statistics["ebu"]["input_i"] = 0
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["ebu"]["input_lra"]
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["ebu"]["input_lra"], 1, 50
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["ebu"]["input_lra"]
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["ebu"]["input_lra"]
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["ebu"]["input_lra"]
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['ebu']['input_lra']}. "
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["ebu"]
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.27.7
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 first pass loudness statistics formatted as JSON to stdout.
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
- "ebu": None,
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
- "ebu": None,
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
- "ebu": None,
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
- "ebu": None,
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
- "ebu": {
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
- "ebu": {
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
- "ebu": {
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
- "ebu": {
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"