ffmpeg-normalize 1.31.2__py2.py3-none-any.whl → 1.32.0__py2.py3-none-any.whl
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/__main__.py +17 -1
- ffmpeg_normalize/_cmd_utils.py +1 -3
- ffmpeg_normalize/_ffmpeg_normalize.py +18 -3
- ffmpeg_normalize/_media_file.py +116 -6
- ffmpeg_normalize/_streams.py +15 -14
- ffmpeg_normalize/_version.py +1 -1
- {ffmpeg_normalize-1.31.2.dist-info → ffmpeg_normalize-1.32.0.dist-info}/LICENSE +1 -1
- {ffmpeg_normalize-1.31.2.dist-info → ffmpeg_normalize-1.32.0.dist-info}/METADATA +40 -563
- ffmpeg_normalize-1.32.0.dist-info/RECORD +16 -0
- ffmpeg_normalize-1.31.2.dist-info/RECORD +0 -16
- {ffmpeg_normalize-1.31.2.dist-info → ffmpeg_normalize-1.32.0.dist-info}/WHEEL +0 -0
- {ffmpeg_normalize-1.31.2.dist-info → ffmpeg_normalize-1.32.0.dist-info}/entry_points.txt +0 -0
- {ffmpeg_normalize-1.31.2.dist-info → ffmpeg_normalize-1.32.0.dist-info}/top_level.txt +0 -0
ffmpeg_normalize/__main__.py
CHANGED
|
@@ -159,6 +159,17 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
159
159
|
action="store_true",
|
|
160
160
|
help="Print loudness statistics for both passes formatted as JSON to stdout.",
|
|
161
161
|
)
|
|
162
|
+
group_normalization.add_argument(
|
|
163
|
+
"--replaygain",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help=textwrap.dedent(
|
|
166
|
+
"""\
|
|
167
|
+
Write ReplayGain tags to the original file without normalizing.
|
|
168
|
+
This mode will overwrite the input file and ignore other options.
|
|
169
|
+
Only works with EBU normalization, and only with .mp3, .mp4/.m4a, .ogg, .opus for now.
|
|
170
|
+
"""
|
|
171
|
+
),
|
|
172
|
+
)
|
|
162
173
|
|
|
163
174
|
# group_normalization.add_argument(
|
|
164
175
|
# '--threshold',
|
|
@@ -562,6 +573,7 @@ def main() -> None:
|
|
|
562
573
|
extension=cli_args.extension,
|
|
563
574
|
dry_run=cli_args.dry_run,
|
|
564
575
|
progress=cli_args.progress,
|
|
576
|
+
replaygain=cli_args.replaygain,
|
|
565
577
|
)
|
|
566
578
|
|
|
567
579
|
if cli_args.output and len(cli_args.input) > len(cli_args.output):
|
|
@@ -595,7 +607,11 @@ def main() -> None:
|
|
|
595
607
|
)
|
|
596
608
|
os.makedirs(cli_args.output_folder, exist_ok=True)
|
|
597
609
|
|
|
598
|
-
if
|
|
610
|
+
if (
|
|
611
|
+
os.path.exists(output_file)
|
|
612
|
+
and not cli_args.force
|
|
613
|
+
and not cli_args.replaygain
|
|
614
|
+
):
|
|
599
615
|
_logger.warning(
|
|
600
616
|
f"Output file '{output_file}' already exists, skipping. Use -f to force overwriting."
|
|
601
617
|
)
|
ffmpeg_normalize/_cmd_utils.py
CHANGED
|
@@ -5,9 +5,8 @@ import os
|
|
|
5
5
|
import re
|
|
6
6
|
import shlex
|
|
7
7
|
import subprocess
|
|
8
|
-
from platform import system
|
|
9
8
|
from shutil import which
|
|
10
|
-
from typing import
|
|
9
|
+
from typing import Any, Iterator
|
|
11
10
|
|
|
12
11
|
from ffmpeg_progress_yield import FfmpegProgress
|
|
13
12
|
|
|
@@ -15,7 +14,6 @@ from ._errors import FFmpegNormalizeError
|
|
|
15
14
|
|
|
16
15
|
_logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
18
|
-
NUL = "NUL" if system() in ("Windows", "cli") else "/dev/null"
|
|
19
17
|
DUR_REGEX = re.compile(
|
|
20
18
|
r"Duration: (?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})"
|
|
21
19
|
)
|
|
@@ -83,6 +83,7 @@ class FFmpegNormalize:
|
|
|
83
83
|
dry_run (bool, optional): Dry run. Defaults to False.
|
|
84
84
|
debug (bool, optional): Debug. Defaults to False.
|
|
85
85
|
progress (bool, optional): Progress. Defaults to False.
|
|
86
|
+
replaygain (bool, optional): Write ReplayGain tags without normalizing. Defaults to False.
|
|
86
87
|
|
|
87
88
|
Raises:
|
|
88
89
|
FFmpegNormalizeError: If the ffmpeg executable is not found or does not support the loudnorm filter.
|
|
@@ -122,6 +123,7 @@ class FFmpegNormalize:
|
|
|
122
123
|
dry_run: bool = False,
|
|
123
124
|
debug: bool = False,
|
|
124
125
|
progress: bool = False,
|
|
126
|
+
replaygain: bool = False,
|
|
125
127
|
):
|
|
126
128
|
self.ffmpeg_exe = get_ffmpeg_exe()
|
|
127
129
|
self.has_loudnorm_capabilities = ffmpeg_has_loudnorm()
|
|
@@ -203,6 +205,7 @@ class FFmpegNormalize:
|
|
|
203
205
|
self.dry_run = dry_run
|
|
204
206
|
self.debug = debug
|
|
205
207
|
self.progress = progress
|
|
208
|
+
self.replaygain = replaygain
|
|
206
209
|
|
|
207
210
|
if (
|
|
208
211
|
self.audio_codec is None or "pcm" in self.audio_codec
|
|
@@ -212,6 +215,12 @@ class FFmpegNormalize:
|
|
|
212
215
|
"Please choose a suitable audio codec with the -c:a option."
|
|
213
216
|
)
|
|
214
217
|
|
|
218
|
+
# replaygain only works for EBU for now
|
|
219
|
+
if self.replaygain and self.normalization_type != "ebu":
|
|
220
|
+
raise FFmpegNormalizeError(
|
|
221
|
+
"ReplayGain only works for EBU normalization type for now."
|
|
222
|
+
)
|
|
223
|
+
|
|
215
224
|
self.stats: list[LoudnessStatisticsWithMetadata] = []
|
|
216
225
|
self.media_files: list[MediaFile] = []
|
|
217
226
|
self.file_count = 0
|
|
@@ -263,8 +272,14 @@ class FFmpegNormalize:
|
|
|
263
272
|
# raise the error so the program will exit
|
|
264
273
|
raise e
|
|
265
274
|
|
|
266
|
-
_logger.info(f"Normalized file written to {media_file.output_file}")
|
|
267
|
-
|
|
268
275
|
if self.print_stats:
|
|
269
|
-
json.dump(
|
|
276
|
+
json.dump(
|
|
277
|
+
list(
|
|
278
|
+
chain.from_iterable(
|
|
279
|
+
media_file.get_stats() for media_file in self.media_files
|
|
280
|
+
)
|
|
281
|
+
),
|
|
282
|
+
sys.stdout,
|
|
283
|
+
indent=4,
|
|
284
|
+
)
|
|
270
285
|
print()
|
ffmpeg_normalize/_media_file.py
CHANGED
|
@@ -8,9 +8,14 @@ from shutil import move, rmtree
|
|
|
8
8
|
from tempfile import mkdtemp
|
|
9
9
|
from typing import TYPE_CHECKING, Iterable, Iterator, Literal, TypedDict
|
|
10
10
|
|
|
11
|
+
from mutagen.id3 import ID3, TXXX
|
|
12
|
+
from mutagen.mp3 import MP3
|
|
13
|
+
from mutagen.mp4 import MP4
|
|
14
|
+
from mutagen.oggopus import OggOpus
|
|
15
|
+
from mutagen.oggvorbis import OggVorbis
|
|
11
16
|
from tqdm import tqdm
|
|
12
17
|
|
|
13
|
-
from ._cmd_utils import DUR_REGEX,
|
|
18
|
+
from ._cmd_utils import DUR_REGEX, CommandRunner
|
|
14
19
|
from ._errors import FFmpegNormalizeError
|
|
15
20
|
from ._streams import (
|
|
16
21
|
AudioStream,
|
|
@@ -69,7 +74,7 @@ class MediaFile:
|
|
|
69
74
|
self.output_file = output_file
|
|
70
75
|
current_ext = os.path.splitext(output_file)[1][1:]
|
|
71
76
|
# we need to check if it's empty, e.g. /dev/null or NUL
|
|
72
|
-
if current_ext == "" or self.output_file ==
|
|
77
|
+
if current_ext == "" or self.output_file == os.devnull:
|
|
73
78
|
self.output_ext = self.ffmpeg_normalize.extension
|
|
74
79
|
else:
|
|
75
80
|
self.output_ext = current_ext
|
|
@@ -114,7 +119,7 @@ class MediaFile:
|
|
|
114
119
|
"0",
|
|
115
120
|
"-f",
|
|
116
121
|
"null",
|
|
117
|
-
|
|
122
|
+
os.devnull,
|
|
118
123
|
]
|
|
119
124
|
|
|
120
125
|
output = CommandRunner().run_command(cmd).get_output()
|
|
@@ -198,6 +203,11 @@ class MediaFile:
|
|
|
198
203
|
# run the first pass to get loudness stats
|
|
199
204
|
self._first_pass()
|
|
200
205
|
|
|
206
|
+
# shortcut to apply replaygain
|
|
207
|
+
if self.ffmpeg_normalize.replaygain:
|
|
208
|
+
self._run_replaygain()
|
|
209
|
+
return
|
|
210
|
+
|
|
201
211
|
# run the second pass as a whole
|
|
202
212
|
if self.ffmpeg_normalize.progress:
|
|
203
213
|
with tqdm(
|
|
@@ -212,6 +222,106 @@ class MediaFile:
|
|
|
212
222
|
for _ in self._second_pass():
|
|
213
223
|
pass
|
|
214
224
|
|
|
225
|
+
_logger.info(f"Normalized file written to {self.output_file}")
|
|
226
|
+
|
|
227
|
+
def _run_replaygain(self) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Run the replaygain process for this file.
|
|
230
|
+
"""
|
|
231
|
+
_logger.debug(f"Running replaygain for {self.input_file}")
|
|
232
|
+
|
|
233
|
+
# get the audio streams
|
|
234
|
+
audio_streams = list(self.streams["audio"].values())
|
|
235
|
+
|
|
236
|
+
# get the loudnorm stats from the first pass
|
|
237
|
+
loudnorm_stats = audio_streams[0].loudness_statistics["ebu_pass1"]
|
|
238
|
+
|
|
239
|
+
if loudnorm_stats is None:
|
|
240
|
+
_logger.error("no loudnorm stats available in first pass stats!")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# apply the replaygain tag from the first audio stream (to all audio streams)
|
|
244
|
+
if len(audio_streams) > 1:
|
|
245
|
+
_logger.warning(
|
|
246
|
+
f"Your input file has {len(audio_streams)} audio streams. "
|
|
247
|
+
"Only the first audio stream's replaygain tag will be applied. "
|
|
248
|
+
"All audio streams will receive the same tag."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
target_level = self.ffmpeg_normalize.target_level
|
|
252
|
+
input_i = loudnorm_stats["input_i"] # Integrated loudness
|
|
253
|
+
input_tp = loudnorm_stats["input_tp"] # True peak
|
|
254
|
+
|
|
255
|
+
if input_i is None or input_tp is None:
|
|
256
|
+
_logger.error("no input_i or input_tp available in first pass stats!")
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
track_gain = -(input_i - target_level) # dB
|
|
260
|
+
track_peak = 10 ** (input_tp / 20) # linear scale
|
|
261
|
+
|
|
262
|
+
_logger.debug(f"Track gain: {track_gain} dB")
|
|
263
|
+
_logger.debug(f"Track peak: {track_peak}")
|
|
264
|
+
|
|
265
|
+
if not self.ffmpeg_normalize.dry_run:
|
|
266
|
+
self._write_replaygain_tags(track_gain, track_peak)
|
|
267
|
+
else:
|
|
268
|
+
_logger.warning("Dry run used, not actually writing replaygain tags")
|
|
269
|
+
|
|
270
|
+
def _write_replaygain_tags(self, track_gain: float, track_peak: float) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Write the replaygain tags to the input file.
|
|
273
|
+
|
|
274
|
+
This is based on the code from bohning/usdb_syncer, licensed under the MIT license.
|
|
275
|
+
See: https://github.com/bohning/usdb_syncer/blob/2fa638c4f487dffe9f5364f91e156ba54cb20233/src/usdb_syncer/resource_dl.py
|
|
276
|
+
"""
|
|
277
|
+
_logger.debug(f"Writing ReplayGain tags to {self.input_file}")
|
|
278
|
+
|
|
279
|
+
input_file_ext = os.path.splitext(self.input_file)[1]
|
|
280
|
+
if input_file_ext == ".mp3":
|
|
281
|
+
mp3 = MP3(self.input_file, ID3=ID3)
|
|
282
|
+
if not mp3.tags:
|
|
283
|
+
return
|
|
284
|
+
mp3.tags.add(
|
|
285
|
+
TXXX(desc="REPLAYGAIN_TRACK_GAIN", text=[f"{track_gain:.2f} dB"])
|
|
286
|
+
)
|
|
287
|
+
mp3.tags.add(TXXX(desc="REPLAYGAIN_TRACK_PEAK", text=[f"{track_peak:.6f}"]))
|
|
288
|
+
mp3.save()
|
|
289
|
+
elif input_file_ext in [".mp4", ".m4a", ".m4v", ".mov"]:
|
|
290
|
+
mp4 = MP4(self.input_file)
|
|
291
|
+
if not mp4.tags:
|
|
292
|
+
mp4.add_tags()
|
|
293
|
+
if not mp4.tags:
|
|
294
|
+
return
|
|
295
|
+
mp4.tags["----:com.apple.iTunes:REPLAYGAIN_TRACK_GAIN"] = [
|
|
296
|
+
f"{track_gain:.2f} dB".encode()
|
|
297
|
+
]
|
|
298
|
+
mp4.tags["----:com.apple.iTunes:REPLAYGAIN_TRACK_PEAK"] = [
|
|
299
|
+
f"{track_peak:.6f}".encode()
|
|
300
|
+
]
|
|
301
|
+
mp4.save()
|
|
302
|
+
elif input_file_ext == ".ogg":
|
|
303
|
+
ogg = OggVorbis(self.input_file)
|
|
304
|
+
ogg["REPLAYGAIN_TRACK_GAIN"] = [f"{track_gain:.2f} dB"]
|
|
305
|
+
ogg["REPLAYGAIN_TRACK_PEAK"] = [f"{track_peak:.6f}"]
|
|
306
|
+
ogg.save()
|
|
307
|
+
elif input_file_ext == ".opus":
|
|
308
|
+
opus = OggOpus(self.input_file)
|
|
309
|
+
# See https://datatracker.ietf.org/doc/html/rfc7845#section-5.2.1
|
|
310
|
+
opus["R128_TRACK_GAIN"] = [str(round(256 * track_gain))]
|
|
311
|
+
opus.save()
|
|
312
|
+
else:
|
|
313
|
+
_logger.error(
|
|
314
|
+
f"Unsupported input file extension: {input_file_ext} for writing replaygain tags. "
|
|
315
|
+
"Only .mp3, .mp4/.m4a, .ogg, .opus are supported. "
|
|
316
|
+
"If you think this should support more formats, please let me know at "
|
|
317
|
+
"https://github.com/slhck/ffmpeg-normalize/issues"
|
|
318
|
+
)
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
_logger.info(
|
|
322
|
+
f"Successfully wrote replaygain tags to input file {self.input_file}"
|
|
323
|
+
)
|
|
324
|
+
|
|
215
325
|
def _can_write_output_video(self) -> bool:
|
|
216
326
|
"""
|
|
217
327
|
Determine whether the output file can contain video at all.
|
|
@@ -435,7 +545,7 @@ class MediaFile:
|
|
|
435
545
|
return
|
|
436
546
|
|
|
437
547
|
# special case: if output is a null device, write directly to it
|
|
438
|
-
if self.output_file ==
|
|
548
|
+
if self.output_file == os.devnull:
|
|
439
549
|
cmd.append(self.output_file)
|
|
440
550
|
else:
|
|
441
551
|
temp_dir = mkdtemp()
|
|
@@ -452,14 +562,14 @@ class MediaFile:
|
|
|
452
562
|
)
|
|
453
563
|
raise e
|
|
454
564
|
else:
|
|
455
|
-
if self.output_file !=
|
|
565
|
+
if self.output_file != os.devnull:
|
|
456
566
|
_logger.debug(
|
|
457
567
|
f"Moving temporary file from {temp_file} to {self.output_file}"
|
|
458
568
|
)
|
|
459
569
|
move(temp_file, self.output_file)
|
|
460
570
|
rmtree(temp_dir, ignore_errors=True)
|
|
461
571
|
except Exception as e:
|
|
462
|
-
if self.output_file !=
|
|
572
|
+
if self.output_file != os.devnull:
|
|
463
573
|
rmtree(temp_dir, ignore_errors=True)
|
|
464
574
|
raise e
|
|
465
575
|
|
ffmpeg_normalize/_streams.py
CHANGED
|
@@ -4,9 +4,9 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
-
from typing import TYPE_CHECKING, Iterator,
|
|
7
|
+
from typing import TYPE_CHECKING, Iterator, Literal, TypedDict, cast
|
|
8
8
|
|
|
9
|
-
from ._cmd_utils import
|
|
9
|
+
from ._cmd_utils import CommandRunner, dict_to_filter_opts
|
|
10
10
|
from ._errors import FFmpegNormalizeError
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -17,6 +17,7 @@ _logger = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
_loudnorm_pattern = re.compile(r"\[Parsed_loudnorm_(\d+)")
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
class EbuLoudnessStatistics(TypedDict):
|
|
21
22
|
input_i: float
|
|
22
23
|
input_tp: float
|
|
@@ -239,7 +240,7 @@ class AudioStream(MediaStream):
|
|
|
239
240
|
"-sn",
|
|
240
241
|
"-f",
|
|
241
242
|
"null",
|
|
242
|
-
|
|
243
|
+
os.devnull,
|
|
243
244
|
]
|
|
244
245
|
|
|
245
246
|
cmd_runner = CommandRunner()
|
|
@@ -310,7 +311,7 @@ class AudioStream(MediaStream):
|
|
|
310
311
|
"-sn",
|
|
311
312
|
"-f",
|
|
312
313
|
"null",
|
|
313
|
-
|
|
314
|
+
os.devnull,
|
|
314
315
|
]
|
|
315
316
|
|
|
316
317
|
cmd_runner = CommandRunner()
|
|
@@ -322,11 +323,13 @@ class AudioStream(MediaStream):
|
|
|
322
323
|
)
|
|
323
324
|
|
|
324
325
|
# only one stream
|
|
325
|
-
self.loudness_statistics["ebu_pass1"] = next(
|
|
326
|
+
self.loudness_statistics["ebu_pass1"] = next(
|
|
327
|
+
iter(AudioStream.prune_and_parse_loudnorm_output(output).values())
|
|
328
|
+
)
|
|
326
329
|
|
|
327
330
|
@staticmethod
|
|
328
331
|
def prune_and_parse_loudnorm_output(
|
|
329
|
-
output: str
|
|
332
|
+
output: str,
|
|
330
333
|
) -> dict[int, EbuLoudnessStatistics]:
|
|
331
334
|
"""
|
|
332
335
|
Prune ffmpeg progress lines from output and parse the loudnorm filter output.
|
|
@@ -344,7 +347,7 @@ class AudioStream(MediaStream):
|
|
|
344
347
|
|
|
345
348
|
@staticmethod
|
|
346
349
|
def _parse_loudnorm_output(
|
|
347
|
-
output_lines: list[str]
|
|
350
|
+
output_lines: list[str],
|
|
348
351
|
) -> dict[int, EbuLoudnessStatistics]:
|
|
349
352
|
"""
|
|
350
353
|
Parse the output of a loudnorm filter to get the EBU loudness statistics.
|
|
@@ -403,7 +406,9 @@ class AudioStream(MediaStream):
|
|
|
403
406
|
# convert to floats
|
|
404
407
|
loudnorm_stats[key] = float(loudnorm_stats[key])
|
|
405
408
|
|
|
406
|
-
result[stream_index] = cast(
|
|
409
|
+
result[stream_index] = cast(
|
|
410
|
+
EbuLoudnessStatistics, loudnorm_stats
|
|
411
|
+
)
|
|
407
412
|
stream_index = -1
|
|
408
413
|
except Exception as e:
|
|
409
414
|
raise FFmpegNormalizeError(
|
|
@@ -504,15 +509,11 @@ class AudioStream(MediaStream):
|
|
|
504
509
|
"offset": self._constrain(
|
|
505
510
|
stats["target_offset"], -99, 99, name="target_offset"
|
|
506
511
|
),
|
|
507
|
-
"measured_i": self._constrain(
|
|
508
|
-
stats["input_i"], -99, 0, name="input_i"
|
|
509
|
-
),
|
|
512
|
+
"measured_i": self._constrain(stats["input_i"], -99, 0, name="input_i"),
|
|
510
513
|
"measured_lra": self._constrain(
|
|
511
514
|
stats["input_lra"], 0, 99, name="input_lra"
|
|
512
515
|
),
|
|
513
|
-
"measured_tp": self._constrain(
|
|
514
|
-
stats["input_tp"], -99, 99, name="input_tp"
|
|
515
|
-
),
|
|
516
|
+
"measured_tp": self._constrain(stats["input_tp"], -99, 99, name="input_tp"),
|
|
516
517
|
"measured_thresh": self._constrain(
|
|
517
518
|
stats["input_thresh"], -99, 0, name="input_thresh"
|
|
518
519
|
),
|
ffmpeg_normalize/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.32.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ffmpeg-normalize
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.32.0
|
|
4
4
|
Summary: Normalize audio via ffmpeg
|
|
5
5
|
Home-page: https://github.com/slhck/ffmpeg-normalize
|
|
6
6
|
Author: Werner Robitza
|
|
@@ -42,557 +42,29 @@ A utility for batch-normalizing audio using ffmpeg.
|
|
|
42
42
|
|
|
43
43
|
This program normalizes media files to a certain loudness level using the EBU R128 loudness normalization procedure. It can also perform RMS-based normalization (where the mean is lifted or attenuated), or peak normalization to a certain target level.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
## ✨ Features
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
- EBU R128 loudness normalization
|
|
48
|
+
- RMS-based normalization
|
|
49
|
+
- Peak normalization
|
|
50
|
+
- Video file support
|
|
51
|
+
- Docker support
|
|
52
|
+
- Python API
|
|
53
|
+
|
|
54
|
+
## 🚀 Quick Start
|
|
48
55
|
|
|
49
56
|
1. Install a recent version of [ffmpeg](https://ffmpeg.org/download.html)
|
|
50
57
|
2. Run `pip3 install ffmpeg-normalize`
|
|
51
58
|
3. Run `ffmpeg-normalize /path/to/your/file.mp4`
|
|
52
|
-
4. Done! 🎧 (the file will be
|
|
53
|
-
|
|
54
|
-
Read on for more info.
|
|
55
|
-
|
|
56
|
-
**Contents:**
|
|
57
|
-
|
|
58
|
-
- [Requirements](#requirements)
|
|
59
|
-
- [ffmpeg](#ffmpeg)
|
|
60
|
-
- [Installation](#installation)
|
|
61
|
-
- [Shell Completions](#shell-completions)
|
|
62
|
-
- [Usage with Docker](#usage-with-docker)
|
|
63
|
-
- [High LeveL Introduction](#high-level-introduction)
|
|
64
|
-
- [Basic Usage](#basic-usage)
|
|
65
|
-
- [Examples](#examples)
|
|
66
|
-
- [Detailed Options](#detailed-options)
|
|
67
|
-
- [File Input/Output](#file-inputoutput)
|
|
68
|
-
- [General](#general)
|
|
69
|
-
- [Normalization](#normalization)
|
|
70
|
-
- [EBU R128 Normalization](#ebu-r128-normalization)
|
|
71
|
-
- [Audio Encoding](#audio-encoding)
|
|
72
|
-
- [Other Encoding Options](#other-encoding-options)
|
|
73
|
-
- [Input/Output Format](#inputoutput-format)
|
|
74
|
-
- [Environment Variables](#environment-variables)
|
|
75
|
-
- [API](#api)
|
|
76
|
-
- [FAQ](#faq)
|
|
77
|
-
- [My output file is too large?](#my-output-file-is-too-large)
|
|
78
|
-
- [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)
|
|
79
|
-
- [The program doesn't work because the "loudnorm" filter can't be found](#the-program-doesnt-work-because-the-loudnorm-filter-cant-be-found)
|
|
80
|
-
- [Should I use this to normalize my music collection?](#should-i-use-this-to-normalize-my-music-collection)
|
|
81
|
-
- [Why are my output files MKV?](#why-are-my-output-files-mkv)
|
|
82
|
-
- [I get a "Could not write header for output file" error](#i-get-a-could-not-write-header-for-output-file-error)
|
|
83
|
-
- [The conversion does not work and I get a cryptic ffmpeg error!](#the-conversion-does-not-work-and-i-get-a-cryptic-ffmpeg-error)
|
|
84
|
-
- [What are the different normalization algorithms?](#what-are-the-different-normalization-algorithms)
|
|
85
|
-
- [Couldn't I just run `loudnorm` with ffmpeg?](#couldnt-i-just-run-loudnorm-with-ffmpeg)
|
|
86
|
-
- [What about speech?](#what-about-speech)
|
|
87
|
-
- [After updating, this program does not work as expected anymore!](#after-updating-this-program-does-not-work-as-expected-anymore)
|
|
88
|
-
- [Can I buy you a beer / coffee / random drink?](#can-i-buy-you-a-beer--coffee--random-drink)
|
|
89
|
-
- [Related Tools and Articles](#related-tools-and-articles)
|
|
90
|
-
- [Contributors](#contributors)
|
|
91
|
-
- [License](#license)
|
|
92
|
-
|
|
93
|
-
-------------
|
|
94
|
-
|
|
95
|
-
## Requirements
|
|
96
|
-
|
|
97
|
-
You need Python 3.9 or higher, and ffmpeg.
|
|
98
|
-
|
|
99
|
-
### ffmpeg
|
|
100
|
-
|
|
101
|
-
- ffmpeg 5.x is required, ffmpeg 6.x is recommended (it fixes [a bug for short files](https://github.com/slhck/ffmpeg-normalize/issues/87))
|
|
102
|
-
- Download a [static build](https://ffmpeg.org/download.html) for your system
|
|
103
|
-
- Place the `ffmpeg` executable in your `$PATH`, or specify the path to the binary with the `FFMPEG_PATH` environment variable in `ffmpeg-normalize`
|
|
104
|
-
|
|
105
|
-
For instance, under Linux:
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
|
109
|
-
mkdir -p ffmpeg
|
|
110
|
-
tar -xf ffmpeg-release-amd64-static.tar.xz -C ffmpeg --strip-components=1
|
|
111
|
-
sudo cp ffmpeg/ffmpeg /usr/local/bin
|
|
112
|
-
sudo cp ffmpeg/ffprobe /usr/local/bin
|
|
113
|
-
sudo chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
For Windows, follow [this guide](https://www.wikihow.com/Install-FFmpeg-on-Windows).
|
|
117
|
-
|
|
118
|
-
For macOS and Linux, you can also use [Homebrew](https://brew.sh):
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
brew install ffmpeg
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Note that using distribution packages (e.g., `apt install ffmpeg`) is not recommended, as these are often outdated.
|
|
125
|
-
|
|
126
|
-
## Installation
|
|
127
|
-
|
|
128
|
-
For Python 3 and pip:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
pip3 install ffmpeg-normalize
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Or download this repository, then run `pip3 install .`.
|
|
135
|
-
|
|
136
|
-
To later upgrade to the latest version, run `pip3 install --upgrade ffmpeg-normalize`.
|
|
137
|
-
|
|
138
|
-
### Shell Completions
|
|
139
|
-
|
|
140
|
-
This tool provides shell completions for bash and zsh. To install them:
|
|
141
|
-
|
|
142
|
-
<!--
|
|
143
|
-
Note to self: Generate the shtab ones with:
|
|
144
|
-
|
|
145
|
-
shtab --shell=bash -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.bash
|
|
146
|
-
shtab --shell=zsh -u ffmpeg_normalize.__main__.create_parser > completions/ffmpeg-normalize-shtab.zsh
|
|
147
|
-
|
|
148
|
-
but these are not properly working yet.
|
|
149
|
-
-->
|
|
150
|
-
|
|
151
|
-
#### Bash
|
|
152
|
-
|
|
153
|
-
If you have [`bash-completion`](https://github.com/scop/bash-completion) installed, you can just copy your new completion script to the `/usr/local/etc/bash_completion.d` directory.
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
|
|
157
|
-
-o /usr/local/etc/bash_completion.d/ffmpeg-normalize
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Without bash-completion, you can manually install the completion script:
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
# create completions directory if it doesn't exist
|
|
164
|
-
mkdir -p ~/.bash_completions.d
|
|
165
|
-
|
|
166
|
-
# download and install completion script
|
|
167
|
-
curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize-completion.bash \
|
|
168
|
-
-o ~/.bash_completions.d/ffmpeg-normalize
|
|
169
|
-
|
|
170
|
-
# source it in your ~/.bashrc
|
|
171
|
-
echo 'source ~/.bash_completions.d/ffmpeg-normalize' >> ~/.bashrc
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
#### Zsh
|
|
175
|
-
|
|
176
|
-
Download the completion script and place it in the default `site-functions` directory:
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
curl -L https://raw.githubusercontent.com/slhck/ffmpeg-normalize/master/completions/ffmpeg-normalize.zsh \
|
|
180
|
-
-o /usr/local/share/zsh/site-functions/
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
You may choose any other directory that is in your `$FPATH` variable.
|
|
184
|
-
Make sure your `.zshrc` file contains `autoload -Uz compinit && compinit`.
|
|
185
|
-
|
|
186
|
-
## Usage with Docker
|
|
187
|
-
|
|
188
|
-
You can use the pre-built image from Docker Hub:
|
|
189
|
-
|
|
190
|
-
```bash
|
|
191
|
-
docker run -v "$(pwd):/tmp" -it slhck/ffmpeg-normalize
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Alternatively, download this repository and run
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
docker build -t ffmpeg-normalize .
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
Then run the container with:
|
|
201
|
-
|
|
202
|
-
```bash
|
|
203
|
-
docker run -v "$(pwd):/tmp" -it ffmpeg-normalize
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
This will mount your current directory to the `/tmp` directory inside the container. Everything else works the same way as if you had installed the program locally. For example, to normalize a file:
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
docker run -v "$(pwd):/tmp" -it ffmpeg-normalize /tmp/yourfile.mp4 -o /tmp/yourfile-normalized.wav
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
You will then find the normalized file in your current directory.
|
|
213
|
-
|
|
214
|
-
## High LeveL Introduction
|
|
215
|
-
|
|
216
|
-
Please read this section for a high level introduction.
|
|
217
|
-
|
|
218
|
-
**What does the program do?**
|
|
219
|
-
|
|
220
|
-
The program takes one or more input files and, by default, writes them to a folder called `normalized`, using an `.mkv` container. All audio streams will be normalized so that they have the same (perceived) volume according to the EBU R128 standard. This is done by analyzing the audio streams and applying a filter to bring them to a target level. Under the hood, the program uses ffmpeg's `loudnorm` filter to do this.
|
|
221
|
-
|
|
222
|
-
**How do I specify the input?**
|
|
223
|
-
|
|
224
|
-
Just give the program one or more input files as arguments. It works with most media files, including video files.
|
|
225
|
-
|
|
226
|
-
**How do I specify the output?**
|
|
227
|
-
|
|
228
|
-
You don't have to specify an output file name (the default is `normalized/<input>.mkv`), but if you want to override it, you can specify one output file name for each input file with the `-o` option. In this case, the container format (e.g. `.wav`) will be inferred from the file name extension that you've given.
|
|
229
|
-
|
|
230
|
-
Example:
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
ffmpeg-normalize 1.wav 2.wav -o 1-normalized.wav 2-normalized.wav
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Note that if you don't specify the output file name for an input file, the container format will be MKV, and the output will be written to `normalized/<input>.mkv`. The reason for choosing the MKV container is that it can handle almost any codec combination.
|
|
237
|
-
|
|
238
|
-
Using the `-ext` option, you can supply a different output extension common to all output files, e.g. `-ext m4a`. However, you need to make sure that the container supports the codecs used for the output (see below).
|
|
239
|
-
|
|
240
|
-
**What will get normalized?**
|
|
241
|
-
|
|
242
|
-
By default, all streams from the input file will be written to the output file. For example, if your input is a video with two language tracks and a subtitle track, both audio tracks will be normalized independently. The video and subtitle tracks will be copied over to the output file.
|
|
243
|
-
|
|
244
|
-
**How will the normalization be done?**
|
|
245
|
-
|
|
246
|
-
The normalization will be performed according to the EBU R128 algorithm with the [`loudnorm` filter](https://ffmpeg.org/ffmpeg-filters.html#loudnorm) from FFmpeg, which was [originally written by Kyle Swanson](https://k.ylo.ph/2016/04/04/loudnorm.html). It will bring the audio to a specified target level. This ensures that multiple files normalized with this filter will have the same perceived loudness.
|
|
247
|
-
|
|
248
|
-
**What codec is chosen?**
|
|
249
|
-
|
|
250
|
-
The default audio encoding method is uncompressed PCM (`pcm_s16le`) to avoid introducing compression artifacts. This will result in a much higher bitrate than you might want, for example if your input files are MP3s.
|
|
251
|
-
|
|
252
|
-
Some containers (like MP4) also cannot handle PCM audio. If you want to use such containers and/or keep the file size down, use `-c:a` and specify an audio codec (e.g., `-c:a aac` for ffmpeg's built-in AAC encoder).
|
|
253
|
-
|
|
254
|
-
## Basic Usage
|
|
255
|
-
|
|
256
|
-
Supply one or more input files, and optionally, output file names:
|
|
257
|
-
|
|
258
|
-
```bash
|
|
259
|
-
ffmpeg-normalize input [input ...][-h][-o OUTPUT [OUTPUT ...]] [options]
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Example:
|
|
263
|
-
|
|
264
|
-
```bash
|
|
265
|
-
ffmpeg-normalize 1.wav 2.wav -o 1-normalized.m4a 2-normalized.m4a -c:a aac -b:a 192k
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
For more information on the options (`[options]`) available, run `ffmpeg-normalize -h`, or read on.
|
|
269
|
-
|
|
270
|
-
## Examples
|
|
271
|
-
|
|
272
|
-
[Read the examples on the wiki.](https://github.com/slhck/ffmpeg-normalize/wiki/examples)
|
|
273
|
-
|
|
274
|
-
## Detailed Options
|
|
275
|
-
|
|
276
|
-
### File Input/Output
|
|
277
|
-
|
|
278
|
-
- `input`: Input media file(s)
|
|
279
|
-
|
|
280
|
-
- `-o OUTPUT [OUTPUT ...], --output OUTPUT [OUTPUT ...]`: Output file names.
|
|
281
|
-
|
|
282
|
-
Will be applied per input file.
|
|
283
|
-
|
|
284
|
-
If no output file name is specified for an input file, the output files
|
|
285
|
-
will be written to the default output folder with the name `<input>.<ext>`, where `<ext>` is the output extension (see `-ext` option).
|
|
286
|
-
|
|
287
|
-
Example: `ffmpeg-normalize 1.wav 2.wav -o 1n.wav 2n.wav`
|
|
288
|
-
|
|
289
|
-
- `-of OUTPUT_FOLDER, --output-folder OUTPUT_FOLDER`: Output folder (default: `normalized`)
|
|
290
|
-
|
|
291
|
-
This folder will be used for input files that have no explicit output name specified.
|
|
292
|
-
|
|
293
|
-
### General
|
|
294
|
-
|
|
295
|
-
- `-f, --force`: Force overwrite existing files
|
|
296
|
-
|
|
297
|
-
- `-d, --debug`: Print debugging output
|
|
298
|
-
|
|
299
|
-
- `-v, --verbose`: Print verbose output
|
|
300
|
-
|
|
301
|
-
- `-q, --quiet`: Only print errors
|
|
302
|
-
|
|
303
|
-
- `-n, --dry-run`: Do not run normalization, only print what would be done
|
|
304
|
-
|
|
305
|
-
- `-pr`, `--progress`: Show progress bar for files and streams
|
|
306
|
-
|
|
307
|
-
- `--version`: Print version and exit
|
|
308
|
-
|
|
309
|
-
### Normalization
|
|
310
|
-
|
|
311
|
-
- `-nt {ebu,rms,peak}, --normalization-type {ebu,rms,peak}`: Normalization type (default: `ebu`).
|
|
312
|
-
|
|
313
|
-
EBU normalization performs two passes and normalizes according to EBU R128.
|
|
314
|
-
|
|
315
|
-
RMS-based normalization brings the input file to the specified RMS level.
|
|
316
|
-
|
|
317
|
-
Peak normalization brings the signal to the specified peak level.
|
|
318
|
-
|
|
319
|
-
- `-t TARGET_LEVEL, --target-level TARGET_LEVEL`: Normalization target level in dB/LUFS (default: -23).
|
|
320
|
-
|
|
321
|
-
For EBU normalization, it corresponds to Integrated Loudness Target in LUFS. The range is -70.0 - -5.0.
|
|
322
|
-
|
|
323
|
-
Otherwise, the range is -99 to 0.
|
|
324
|
-
|
|
325
|
-
- `-p, --print-stats`: Print loudness statistics for both passes formatted as JSON to stdout.
|
|
326
|
-
|
|
327
|
-
### EBU R128 Normalization
|
|
328
|
-
|
|
329
|
-
- `-lrt LOUDNESS_RANGE_TARGET, --loudness-range-target LOUDNESS_RANGE_TARGET`: EBU Loudness Range Target in LUFS (default: 7.0).
|
|
330
|
-
|
|
331
|
-
Range is 1.0 - 50.0.
|
|
332
|
-
|
|
333
|
-
- `--keep-loudness-range-target`: Keep the input loudness range target to allow for linear normalization.
|
|
334
|
-
|
|
335
|
-
- `--keep-lra-above-loudness-range-target`: Keep input loudness range above loudness range target.
|
|
336
|
-
|
|
337
|
-
Can be used as an alternative to `--keep-loudness-range-target` to allow for linear normalization.
|
|
338
|
-
|
|
339
|
-
- `-tp TRUE_PEAK, --true-peak TRUE_PEAK`: EBU Maximum True Peak in dBTP (default: -2.0).
|
|
340
|
-
|
|
341
|
-
Range is -9.0 - +0.0.
|
|
342
|
-
|
|
343
|
-
- `--offset OFFSET`: EBU Offset Gain (default: 0.0).
|
|
344
|
-
|
|
345
|
-
The gain is applied before the true-peak limiter in the first pass only. The offset for the second pass will be automatically determined based on the first pass statistics.
|
|
346
|
-
|
|
347
|
-
Range is -99.0 - +99.0.
|
|
348
|
-
|
|
349
|
-
- `--lower-only`: Whether the audio should not increase in loudness.
|
|
350
|
-
|
|
351
|
-
If the measured loudness from the first pass is lower than the target loudness then normalization pass will be skipped for the measured audio source.
|
|
352
|
-
|
|
353
|
-
- `--auto-lower-loudness-target`: Automatically lower EBU Integrated Loudness Target.
|
|
354
|
-
|
|
355
|
-
Automatically lower EBU Integrated Loudness Target to prevent falling back to dynamic filtering.
|
|
356
|
-
|
|
357
|
-
Makes sure target loudness is lower than measured loudness minus peak loudness (input_i - input_tp) by a small amount.
|
|
358
|
-
|
|
359
|
-
- `--dual-mono`: Treat mono input files as "dual-mono".
|
|
360
|
-
|
|
361
|
-
If a mono file is intended for playback on a stereo system, its EBU R128 measurement will be perceptually incorrect. If set, this option will compensate for this effect. Multi-channel input files are not affected by this option.
|
|
362
|
-
|
|
363
|
-
- `--dynamic`: Force dynamic normalization mode.
|
|
59
|
+
4. Done! 🎧 (the normalized file will be called `normalized/file.mkv`)
|
|
364
60
|
|
|
365
|
-
|
|
61
|
+
## 📓 Documentation
|
|
366
62
|
|
|
367
|
-
|
|
63
|
+
Check out our [documentation](https://slhck.info/ffmpeg-normalize/) for more info!
|
|
368
64
|
|
|
369
|
-
|
|
65
|
+
## 🤝 Contributors
|
|
370
66
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
See `ffmpeg -encoders` for a list.
|
|
374
|
-
|
|
375
|
-
Will use PCM audio with input stream bit depth by default.
|
|
376
|
-
|
|
377
|
-
- `-b:a AUDIO_BITRATE, --audio-bitrate AUDIO_BITRATE`: Audio bitrate in bits/s, or with K suffix.
|
|
378
|
-
|
|
379
|
-
If not specified, will use codec default.
|
|
380
|
-
|
|
381
|
-
- `-ar SAMPLE_RATE, --sample-rate SAMPLE_RATE`: Audio sample rate to use for output files in Hz.
|
|
382
|
-
|
|
383
|
-
Will use input sample rate by default, except for EBU normalization, which will change the input sample rate to 192 kHz.
|
|
384
|
-
|
|
385
|
-
- `-ac`, `--audio-channels`: Set the number of audio channels. If not specified, the input channel layout will be used. This is equivalent to `-ac` in ffmpeg.
|
|
386
|
-
|
|
387
|
-
- `-koa, --keep-original-audio`: Copy original, non-normalized audio streams to output file
|
|
388
|
-
|
|
389
|
-
- `-prf PRE_FILTER, --pre-filter PRE_FILTER`: Add an audio filter chain before applying normalization.
|
|
390
|
-
|
|
391
|
-
Multiple filters can be specified by comma-separating them.
|
|
392
|
-
|
|
393
|
-
- `-pof POST_FILTER, --post-filter POST_FILTER`: Add an audio filter chain after applying normalization.
|
|
394
|
-
|
|
395
|
-
Multiple filters can be specified by comma-separating them.
|
|
396
|
-
|
|
397
|
-
For EBU, the filter will be applied during the second pass.
|
|
398
|
-
|
|
399
|
-
### Other Encoding Options
|
|
400
|
-
|
|
401
|
-
- `-vn, --video-disable`: Do not write video streams to output
|
|
402
|
-
|
|
403
|
-
- `-c:v VIDEO_CODEC, --video-codec VIDEO_CODEC`: Video codec to use for output files (default: 'copy').
|
|
404
|
-
|
|
405
|
-
See `ffmpeg -encoders` for a list.
|
|
406
|
-
|
|
407
|
-
Will attempt to copy video codec by default.
|
|
408
|
-
|
|
409
|
-
- `-sn, --subtitle-disable`: Do not write subtitle streams to output
|
|
410
|
-
|
|
411
|
-
- `-mn, --metadata-disable`: Do not write metadata to output
|
|
412
|
-
|
|
413
|
-
- `-cn, --chapters-disable`: Do not write chapters to output
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
### Input/Output Format
|
|
417
|
-
|
|
418
|
-
- `-ei EXTRA_INPUT_OPTIONS, --extra-input-options EXTRA_INPUT_OPTIONS`: Extra input options list.
|
|
419
|
-
|
|
420
|
-
A list of extra ffmpeg command line arguments valid for the input, applied before ffmpeg's `-i`.
|
|
421
|
-
|
|
422
|
-
You can either use a JSON-formatted list (i.e., a list of comma-separated, quoted elements within square brackets), or a simple string of space-separated arguments.
|
|
423
|
-
|
|
424
|
-
If JSON is used, you need to wrap the whole argument in quotes to prevent shell expansion and to preserve literal quotes inside the string. If a simple string is used, you need to specify the argument with `-e=`.
|
|
425
|
-
|
|
426
|
-
Examples: `-ei '[ "-f", "mpegts", "-r", "24" ]'` or `-ei="-f mpegts -r 24"`
|
|
427
|
-
|
|
428
|
-
- `-e EXTRA_OUTPUT_OPTIONS, --extra-output-options EXTRA_OUTPUT_OPTIONS`: Extra output options list.
|
|
429
|
-
|
|
430
|
-
A list of extra ffmpeg command line arguments valid for the output.
|
|
431
|
-
|
|
432
|
-
You can either use a JSON-formatted list (i.e., a list of comma-separated, quoted elements within square brackets), or a simple string of space-separated arguments.
|
|
433
|
-
|
|
434
|
-
If JSON is used, you need to wrap the whole argument in quotes to prevent shell expansion and to preserve literal quotes inside the string. If a simple string is used, you need to specify the argument with `-e=`.
|
|
435
|
-
|
|
436
|
-
Examples: `-e '[ "-vbr", "3", "-preset:v", "ultrafast" ]'` or `-e="-vbr 3 -preset:v ultrafast"`
|
|
437
|
-
|
|
438
|
-
- `-ofmt OUTPUT_FORMAT, --output-format OUTPUT_FORMAT`: Media format to use for output file(s).
|
|
439
|
-
|
|
440
|
-
See `ffmpeg -formats` for a list.
|
|
441
|
-
|
|
442
|
-
If not specified, the format will be inferred by ffmpeg from the output file name. If the output file name is not explicitly specified, the extension will govern the format (see '--extension' option).
|
|
443
|
-
|
|
444
|
-
- `-ext EXTENSION, --extension EXTENSION`: Output file extension to use for output files that were not explicitly specified. (Default: `mkv`)
|
|
445
|
-
|
|
446
|
-
### Environment Variables
|
|
447
|
-
|
|
448
|
-
The program additionally respects environment variables:
|
|
449
|
-
|
|
450
|
-
- `TMP` / `TEMP` / `TMPDIR`
|
|
451
|
-
|
|
452
|
-
Sets the path to the temporary directory in which files are
|
|
453
|
-
stored before being moved to the final output directory.
|
|
454
|
-
Note: You need to use full paths.
|
|
455
|
-
|
|
456
|
-
- `FFMPEG_PATH`
|
|
457
|
-
|
|
458
|
-
Sets the full path to an `ffmpeg` executable other than
|
|
459
|
-
the system default or you can provide a file name available on $PATH
|
|
460
|
-
|
|
461
|
-
## API
|
|
462
|
-
|
|
463
|
-
This program has a simple API that can be used to integrate it into other Python programs.
|
|
464
|
-
|
|
465
|
-
For more information see the [API documentation](https://htmlpreview.github.io/?https://github.com/slhck/ffmpeg-normalize/blob/master/docs/ffmpeg_normalize.html).
|
|
466
|
-
|
|
467
|
-
## FAQ
|
|
468
|
-
|
|
469
|
-
### My output file is too large?
|
|
470
|
-
|
|
471
|
-
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`.
|
|
472
|
-
|
|
473
|
-
For example:
|
|
474
|
-
|
|
475
|
-
```bash
|
|
476
|
-
ffmpeg-normalize input.wav -o output.m4a -c:a aac -b:a 192k
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### What options should I choose for the EBU R128 filter? What is linear and dynamic mode?
|
|
480
|
-
|
|
481
|
-
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:
|
|
482
|
-
|
|
483
|
-
* Integrated Loudness (I): The overall loudness of the entire audio.
|
|
484
|
-
* Loudness Range (LRA): The variation in loudness over time.
|
|
485
|
-
* True Peak (TP): The maximum level of the audio signal.
|
|
486
|
-
|
|
487
|
-
The normalization process involves measuring these values (input) and then applying gain adjustments to meet target levels (output), typically -23 LUFS for integrated loudness. You can also specify a target loudness range (LRA) and true peak level (TP).
|
|
488
|
-
|
|
489
|
-
**Linear mode** applies a constant gain adjustment across the entire audio file. This is generally preferred because:
|
|
490
|
-
|
|
491
|
-
* It preserves the original dynamic range of the audio.
|
|
492
|
-
* It maintains the relative loudness between different parts of the audio.
|
|
493
|
-
* It avoids potential artifacts or pumping effects that can occur with dynamic processing.
|
|
494
|
-
|
|
495
|
-
**Dynamic mode**, on the other hand, can change the volume dynamically throughout the file. While this can achieve more consistent loudness, it may alter the original artistic intent and potentially introduce audible artifacts (possibly due to some bugs in the ffmpeg filter).
|
|
496
|
-
|
|
497
|
-
For most cases, linear mode is recommended. Dynamic mode should only be used when linear mode is not suitable or when a specific effect is desired. In some cases, `loudnorm` will still fall back to dynamic mode, and a warning will be printed to the console. Here's when this can happen:
|
|
498
|
-
|
|
499
|
-
* When the input loudness range (LRA) is larger than the target loudness range: If the input file has a loudness range that exceeds the specified loudness range target, the loudnorm filter will automatically switch to dynamic mode. This is because linear normalization alone cannot reduce the loudness range without dynamic processing (limiting). The `--keep-loudness-range-target` option can be used to keep the input loudness range target above the specified target.
|
|
500
|
-
|
|
501
|
-
* When the required gain adjustment to meet the integrated loudness target would result in the true peak exceeding the specified true peak limit. This is because linear processing alone cannot reduce peaks without affecting the entire signal. For example, if a file needs to be amplified by 6 dB to reach the target integrated loudness, but doing so would push the true peak above the specified limit, the filter might switch to dynamic mode to handle this situation. If your content allows for it, you can increase the true peak target to give more headroom for linear processing. If you're consistently running into true peak issues, you might also consider lowering your target integrated loudness level.
|
|
502
|
-
|
|
503
|
-
At this time, the `loudnorm` filter in ffmpeg does not provide a way to force linear mode when the input loudness range exceeds the target or when the true peak would be exceeded. There are some options to mitigate this:
|
|
504
|
-
|
|
505
|
-
- The `--keep-lra-above-loudness-range-target` option can be used to keep the input loudness range above the specified target, but it will not force linear mode in all cases.
|
|
506
|
-
- Similarly, the `--keep-loudness-range-target` option can be used to keep the input loudness range target.
|
|
507
|
-
- The `--lower-only` option can be used to skip the normalization pass completely if the measured loudness is lower than the target loudness.
|
|
508
|
-
|
|
509
|
-
### The program doesn't work because the "loudnorm" filter can't be found
|
|
510
|
-
|
|
511
|
-
Make sure you run a recent ffmpeg version and that `loudnorm` is part of the output when you run `ffmpeg -filters`. Many distributions package outdated ffmpeg versions, or (even worse), Libav's `ffmpeg` disguising as a real `ffmpeg` from the FFmpeg project.
|
|
512
|
-
|
|
513
|
-
Some ffmpeg builds also do not have the `loudnorm` filter enabled.
|
|
514
|
-
|
|
515
|
-
You can always download a static build from [their website](http://ffmpeg.org/download.html) and use that.
|
|
516
|
-
|
|
517
|
-
If you have to use an outdated ffmpeg version, you can only use `rms` or `peak` as normalization types, but I can't promise that the program will work correctly.
|
|
518
|
-
|
|
519
|
-
### Should I use this to normalize my music collection?
|
|
520
|
-
|
|
521
|
-
Generally, no.
|
|
522
|
-
|
|
523
|
-
When you run `ffmpeg-normalize` and re-encode files with MP3 or AAC, you will inevitably introduce [generation loss](https://en.wikipedia.org/wiki/Generation_loss). Therefore, I do not recommend running this on your precious music collection, unless you have a backup of the originals or accept potential quality reduction. If you just want to normalize the subjective volume of the files without changing the actual content, consider using [MP3Gain](http://mp3gain.sourceforge.net/) and [aacgain](http://aacgain.altosdesign.com/).
|
|
524
|
-
|
|
525
|
-
### Why are my output files MKV?
|
|
526
|
-
|
|
527
|
-
I chose MKV as a default output container since it handles almost every possible combination of audio, video, and subtitle codecs. If you know which audio/video codec you want, and which container is supported, use the output options to specify the encoder and output file name manually.
|
|
528
|
-
|
|
529
|
-
### I get a "Could not write header for output file" error
|
|
530
|
-
|
|
531
|
-
See the [next section](#the-conversion-does-not-work-and-i-get-a-cryptic-ffmpeg-error).
|
|
532
|
-
|
|
533
|
-
### The conversion does not work and I get a cryptic ffmpeg error!
|
|
534
|
-
|
|
535
|
-
Maybe ffmpeg says something like:
|
|
536
|
-
|
|
537
|
-
> Could not write header for output file #0 (incorrect codec parameters ?): Invalid argument
|
|
538
|
-
|
|
539
|
-
Or the program says:
|
|
540
|
-
|
|
541
|
-
> … Please choose a suitable audio codec with the `-c:a` option.
|
|
542
|
-
|
|
543
|
-
One possible reason is that the input file contains some streams that cannot be mapped to the output file, or that you are using a codec that does not work for the output file. Examples:
|
|
544
|
-
|
|
545
|
-
- You are trying to normalize a movie file, writing to a `.wav` or `.mp3` file. WAV/MP3 files only support audio, not video. Disable video and subtitles with `-vn` and `-sn`, or choose a container that supports video (e.g. `.mkv`).
|
|
546
|
-
|
|
547
|
-
- You are trying to normalize a file, writing to an `.mp4` container. This program defaults to PCM audio, but MP4 does not support PCM audio. Make sure that your audio codec is set to something MP4 containers support (e.g. `-c:a aac`).
|
|
548
|
-
|
|
549
|
-
The default output container is `.mkv` as it will support most input stream types. If you want a different output container, [make sure that it supports](https://en.wikipedia.org/wiki/Comparison_of_container_file_formats) your input file's video, audio, and subtitle streams (if any).
|
|
550
|
-
|
|
551
|
-
Also, if there is some other broken metadata, you can try to disable copying over of metadata with `-mn`.
|
|
552
|
-
|
|
553
|
-
Finally, make sure you use a recent version of ffmpeg. The [static builds](https://ffmpeg.org/download.html) are usually the best option.
|
|
554
|
-
|
|
555
|
-
### What are the different normalization algorithms?
|
|
556
|
-
|
|
557
|
-
- **EBU R128** is an EBU standard that is commonly used in the broadcasting world. The normalization is performed using a psychoacoustic model that targets a subjective loudness level measured in LUFS (Loudness Unit Full Scale). R128 is subjectively more accurate than any peak or RMS-based normalization. More info on R128 can be found in the [official document](https://tech.ebu.ch/docs/r/r128.pdf) and [the `loudnorm` filter description](http://k.ylo.ph/2016/04/04/loudnorm.html) by its original author.
|
|
558
|
-
|
|
559
|
-
- **Peak Normalization** analyzes the peak signal level in dBFS and increases the volume of the input signal such that the maximum in the output is 0 dB (or any other chosen threshold). Since spikes in the signal can cause high volume peaks, peak normalization might still result in files that are subjectively quieter than other, non-peak-normalized files.
|
|
560
|
-
|
|
561
|
-
- **RMS-based Normalization** analyzes the [RMS power](https://en.wikipedia.org/wiki/Root_mean_square#Average_power) of the signal and changes the volume such that a new RMS target is reached. Otherwise it works similar to peak normalization.
|
|
562
|
-
|
|
563
|
-
### Couldn't I just run `loudnorm` with ffmpeg?
|
|
564
|
-
|
|
565
|
-
You absolutely can. However, you can get better accuracy and linear normalization with two passes of the filter. Since ffmpeg does not allow you to automatically run these two passes, you have to do it yourself and parse the output values from the first run.
|
|
566
|
-
|
|
567
|
-
If ffmpeg-normalize is too over-engineered for you, you could also use an approach such as featured [in this Ruby script](https://gist.github.com/kylophone/84ba07f6205895e65c9634a956bf6d54) that performs the two `loudnorm` passes.
|
|
568
|
-
|
|
569
|
-
If you want dynamic normalization (the loudnorm default), simply use ffmpeg with one pass, e.g.:
|
|
570
|
-
|
|
571
|
-
```bash
|
|
572
|
-
ffmpeg -i input.mp3 -af loudnorm -c:a aac -b:a 192k output.m4a
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
### What about speech?
|
|
576
|
-
|
|
577
|
-
You should check out the `speechnorm` filter that is part of ffmpeg. It is a designed to be used in one pass, so you don't need this script at all.
|
|
578
|
-
|
|
579
|
-
See [the documentation](https://ffmpeg.org/ffmpeg-all.html#speechnorm) for more information.
|
|
580
|
-
|
|
581
|
-
### After updating, this program does not work as expected anymore!
|
|
582
|
-
|
|
583
|
-
You are probably using a 0.x version of this program. There are significant changes to the command line arguments and inner workings of this program, so please adapt your scripts to the new one. Those changes were necessary to address a few issues that kept piling up; leaving the program as-is would have made it hard to extend it. You can continue using the old version (find it under *Releases* on GitHub or request the specific version from PyPi), but it will not be supported anymore.
|
|
584
|
-
|
|
585
|
-
### Can I buy you a beer / coffee / random drink?
|
|
586
|
-
|
|
587
|
-
If you found this program useful and feel like giving back, feel free to send a donation [via PayPal](https://paypal.me/WernerRobitza).
|
|
588
|
-
|
|
589
|
-
## Related Tools and Articles
|
|
590
|
-
|
|
591
|
-
- [Create an AppleScript application to drop or open a folder of files in ffmpeg-normalize](https://prehensileblog.wordpress.com/2022/04/15/create-an-applescript-application-to-drop-or-open-a-folder-of-files-in-ffmpeg-normalize/)
|
|
592
|
-
|
|
593
|
-
*(Have a link? Please propose an edit to this section via a pull request!)*
|
|
594
|
-
|
|
595
|
-
## Contributors
|
|
67
|
+
The only reason this project exists in its current form is because [@benjaoming](https://github.com/slhck/ffmpeg-normalize/issues?q=is%3Apr+author%3Abenjaoming)'s initial PRs. Thanks for everyone's support!
|
|
596
68
|
|
|
597
69
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
598
70
|
<!-- prettier-ignore-start -->
|
|
@@ -642,33 +114,38 @@ If you found this program useful and feel like giving back, feel free to send a
|
|
|
642
114
|
|
|
643
115
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
644
116
|
|
|
645
|
-
## License
|
|
646
117
|
|
|
647
|
-
|
|
118
|
+
# Changelog
|
|
648
119
|
|
|
649
|
-
Copyright (c) 2015-2022 Werner Robitza
|
|
650
120
|
|
|
651
|
-
|
|
652
|
-
copy of this software and associated documentation files (the
|
|
653
|
-
"Software"), to deal in the Software without restriction, including
|
|
654
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
655
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
656
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
657
|
-
the following conditions:
|
|
121
|
+
## v1.32.0 (2025-05-07)
|
|
658
122
|
|
|
659
|
-
|
|
660
|
-
in all copies or substantial portions of the Software.
|
|
123
|
+
* Docs: update contributing guide.
|
|
661
124
|
|
|
662
|
-
|
|
663
|
-
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
664
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
665
|
-
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
666
|
-
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
667
|
-
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
668
|
-
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
125
|
+
* Docs: convert options to headlines.
|
|
669
126
|
|
|
127
|
+
* Docs: update ffmpeg guide.
|
|
670
128
|
|
|
671
|
-
|
|
129
|
+
* Fix: do not write replaygain tags if dry run is used.
|
|
130
|
+
|
|
131
|
+
* Fix: flake8 no longer used.
|
|
132
|
+
|
|
133
|
+
* Fix docs links.
|
|
134
|
+
|
|
135
|
+
* Add mkdocs documentation.
|
|
136
|
+
|
|
137
|
+
* Update python in dockerfile.
|
|
138
|
+
|
|
139
|
+
* Remove years from copyright.
|
|
140
|
+
|
|
141
|
+
* Dev stuff.
|
|
142
|
+
|
|
143
|
+
* Add replaygain support.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
## v1.31.3 (2025-04-14)
|
|
147
|
+
|
|
148
|
+
* Swap NUL to os.devnull.
|
|
672
149
|
|
|
673
150
|
|
|
674
151
|
## v1.31.2 (2025-03-19)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
ffmpeg_normalize/__init__.py,sha256=aAhlk93ZE6SQcWUDzZQcw9vJh0bJcKEUNFGhVc5ZIto,453
|
|
2
|
+
ffmpeg_normalize/__main__.py,sha256=LMj4Xl140ZKe5naY36fAwzz72lK59srzkVlbb0KzkcQ,19955
|
|
3
|
+
ffmpeg_normalize/_cmd_utils.py,sha256=iGzO3iOylDUOnx-FCKd84BMxiIhmIthxU1tg7kvf4Ss,5269
|
|
4
|
+
ffmpeg_normalize/_errors.py,sha256=brTQ4osJ4fTA8wnyMPVVYfGwJ0wqeShRFydTEwi_VEY,48
|
|
5
|
+
ffmpeg_normalize/_ffmpeg_normalize.py,sha256=79wzFR4ZOgy-Wn0ywi8PNJzsrxiDSMKtJ6_auHxiQvo,11762
|
|
6
|
+
ffmpeg_normalize/_logger.py,sha256=3Ap4Fxg7xGrzz7h4IGuNEf0KKstx0Rq_eLbHPrHzcrI,1841
|
|
7
|
+
ffmpeg_normalize/_media_file.py,sha256=aQcVWTMwbTtb23yAHyBsbnDx4R5l3_bsPflhkYxKTEA,23354
|
|
8
|
+
ffmpeg_normalize/_streams.py,sha256=C0b8GT4EK0W02x8pM99H1-BjjO-pLxYFNvZ4R7gu4CM,20223
|
|
9
|
+
ffmpeg_normalize/_version.py,sha256=ioioljX8i89Tf7eUgtSNcZwCc54nD1e2hO3ZGc960a0,23
|
|
10
|
+
ffmpeg_normalize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
ffmpeg_normalize-1.32.0.dist-info/LICENSE,sha256=mw5RQE6v4UXG_d2gYIQw9rq6jYWQCtzIs3fSm5sBSrs,1076
|
|
12
|
+
ffmpeg_normalize-1.32.0.dist-info/METADATA,sha256=7nxBtY_JpfOZ1bRkribCNVI_k7p1W0L6gGc5yWU0iOw,32543
|
|
13
|
+
ffmpeg_normalize-1.32.0.dist-info/WHEEL,sha256=fS9sRbCBHs7VFcwJLnLXN1MZRR0_TVTxvXKzOnaSFs8,110
|
|
14
|
+
ffmpeg_normalize-1.32.0.dist-info/entry_points.txt,sha256=X0EC5ptb0iGOxrk3Aa65dVQtvUixngLd_2-iAtSixdc,68
|
|
15
|
+
ffmpeg_normalize-1.32.0.dist-info/top_level.txt,sha256=wnUkr17ckPrrU1JsxZQiXbEBUnHKsC64yck-MemEBuI,17
|
|
16
|
+
ffmpeg_normalize-1.32.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
ffmpeg_normalize/__init__.py,sha256=aAhlk93ZE6SQcWUDzZQcw9vJh0bJcKEUNFGhVc5ZIto,453
|
|
2
|
-
ffmpeg_normalize/__main__.py,sha256=qOiycud87-wBceNLujaPZgWCTwJr7kaJM-IBvz4D7f4,19431
|
|
3
|
-
ffmpeg_normalize/_cmd_utils.py,sha256=S7PLXQAZHmJ30RM9K6b--vXuxMf-cQHtaFOPtILxz-4,5360
|
|
4
|
-
ffmpeg_normalize/_errors.py,sha256=brTQ4osJ4fTA8wnyMPVVYfGwJ0wqeShRFydTEwi_VEY,48
|
|
5
|
-
ffmpeg_normalize/_ffmpeg_normalize.py,sha256=JIVE03ZJi-jOvnc7OuftQEqloQwwyO7ZD3PIMFmIvbo,11283
|
|
6
|
-
ffmpeg_normalize/_logger.py,sha256=3Ap4Fxg7xGrzz7h4IGuNEf0KKstx0Rq_eLbHPrHzcrI,1841
|
|
7
|
-
ffmpeg_normalize/_media_file.py,sha256=ko5Jn0OxaDsIHbsm-CKWu96chf-8CA5aVUcxGUrPTEU,18863
|
|
8
|
-
ffmpeg_normalize/_streams.py,sha256=LIllXl4SKLxlyPVjD3ieHqc_byF2eUTjnK-clh2g_CY,20211
|
|
9
|
-
ffmpeg_normalize/_version.py,sha256=6MP_NMOhYOl3gX-No5ZuL2yHjzinJzxgtxqL3H5MNuc,23
|
|
10
|
-
ffmpeg_normalize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
ffmpeg_normalize-1.31.2.dist-info/LICENSE,sha256=zeqAHGWrSIwdPHsZMZv1_N0gGFO1xxjcZEz9CplR4EM,1086
|
|
12
|
-
ffmpeg_normalize-1.31.2.dist-info/METADATA,sha256=CJJ8-L3WwkbJgfY_MBewdmtjGJmsMRcHRt62pXKj9DU,60274
|
|
13
|
-
ffmpeg_normalize-1.31.2.dist-info/WHEEL,sha256=fS9sRbCBHs7VFcwJLnLXN1MZRR0_TVTxvXKzOnaSFs8,110
|
|
14
|
-
ffmpeg_normalize-1.31.2.dist-info/entry_points.txt,sha256=X0EC5ptb0iGOxrk3Aa65dVQtvUixngLd_2-iAtSixdc,68
|
|
15
|
-
ffmpeg_normalize-1.31.2.dist-info/top_level.txt,sha256=wnUkr17ckPrrU1JsxZQiXbEBUnHKsC64yck-MemEBuI,17
|
|
16
|
-
ffmpeg_normalize-1.31.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|