ffmpeg-normalize 1.35.0__py3-none-any.whl → 1.36.0__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 +62 -17
- ffmpeg_normalize/_ffmpeg_normalize.py +42 -0
- ffmpeg_normalize/_presets.py +256 -0
- ffmpeg_normalize/data/presets/music.json +6 -0
- ffmpeg_normalize/data/presets/podcast.json +7 -0
- ffmpeg_normalize/data/presets/streaming-video.json +7 -0
- {ffmpeg_normalize-1.35.0.dist-info → ffmpeg_normalize-1.36.0.dist-info}/METADATA +11 -1
- ffmpeg_normalize-1.36.0.dist-info/RECORD +18 -0
- ffmpeg_normalize-1.35.0.dist-info/RECORD +0 -14
- {ffmpeg_normalize-1.35.0.dist-info → ffmpeg_normalize-1.36.0.dist-info}/WHEEL +0 -0
- {ffmpeg_normalize-1.35.0.dist-info → ffmpeg_normalize-1.36.0.dist-info}/entry_points.txt +0 -0
- {ffmpeg_normalize-1.35.0.dist-info → ffmpeg_normalize-1.36.0.dist-info}/licenses/LICENSE.md +0 -0
ffmpeg_normalize/__main__.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import NoReturn
|
|
|
13
13
|
from ._errors import FFmpegNormalizeError
|
|
14
14
|
from ._ffmpeg_normalize import NORMALIZATION_TYPES, FFmpegNormalize
|
|
15
15
|
from ._logger import setup_cli_logger
|
|
16
|
+
from ._presets import PresetManager
|
|
16
17
|
|
|
17
18
|
# Import version from package
|
|
18
19
|
import importlib.metadata
|
|
@@ -50,7 +51,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
50
51
|
|
|
51
52
|
Author: Werner Robitza
|
|
52
53
|
License: MIT
|
|
53
|
-
|
|
54
|
+
Website / Issues: https://github.com/slhck/ffmpeg-normalize
|
|
54
55
|
"""
|
|
55
56
|
),
|
|
56
57
|
)
|
|
@@ -90,7 +91,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
90
91
|
name specified.
|
|
91
92
|
"""
|
|
92
93
|
),
|
|
93
|
-
default="
|
|
94
|
+
default=FFmpegNormalize.DEFAULTS["output_folder"],
|
|
94
95
|
)
|
|
95
96
|
|
|
96
97
|
group_general = parser.add_argument_group("General Options")
|
|
@@ -124,6 +125,28 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
124
125
|
version=f"%(prog)s v{__version__}",
|
|
125
126
|
help="Print version and exit",
|
|
126
127
|
)
|
|
128
|
+
group_general.add_argument(
|
|
129
|
+
"--preset",
|
|
130
|
+
type=str,
|
|
131
|
+
help=textwrap.dedent(
|
|
132
|
+
"""\
|
|
133
|
+
Load options from a preset file.
|
|
134
|
+
|
|
135
|
+
Preset files are JSON files located in the presets directory.
|
|
136
|
+
The directory location depends on your OS:
|
|
137
|
+
- Linux/macOS: ~/.config/ffmpeg-normalize/presets/
|
|
138
|
+
- Windows: %%APPDATA%%/ffmpeg-normalize/presets/
|
|
139
|
+
|
|
140
|
+
Use --list-presets to see available presets.
|
|
141
|
+
CLI options specified on the command line take precedence over preset values.
|
|
142
|
+
"""
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
group_general.add_argument(
|
|
146
|
+
"--list-presets",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help="List all available presets and exit",
|
|
149
|
+
)
|
|
127
150
|
|
|
128
151
|
group_normalization = parser.add_argument_group("Normalization")
|
|
129
152
|
group_normalization.add_argument(
|
|
@@ -144,15 +167,15 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
144
167
|
Peak normalization brings the signal to the specified peak level.
|
|
145
168
|
"""
|
|
146
169
|
),
|
|
147
|
-
default="
|
|
170
|
+
default=FFmpegNormalize.DEFAULTS["normalization_type"],
|
|
148
171
|
)
|
|
149
172
|
group_normalization.add_argument(
|
|
150
173
|
"-t",
|
|
151
174
|
"--target-level",
|
|
152
175
|
type=float,
|
|
153
176
|
help=textwrap.dedent(
|
|
154
|
-
"""\
|
|
155
|
-
Normalization target level in dB/LUFS (default:
|
|
177
|
+
f"""\
|
|
178
|
+
Normalization target level in dB/LUFS (default: {FFmpegNormalize.DEFAULTS["target_level"]}).
|
|
156
179
|
|
|
157
180
|
For EBU normalization, it corresponds to Integrated Loudness Target
|
|
158
181
|
in LUFS. The range is -70.0 - -5.0.
|
|
@@ -160,7 +183,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
160
183
|
Otherwise, the range is -99 to 0.
|
|
161
184
|
"""
|
|
162
185
|
),
|
|
163
|
-
default
|
|
186
|
+
default=FFmpegNormalize.DEFAULTS["target_level"],
|
|
164
187
|
)
|
|
165
188
|
group_normalization.add_argument(
|
|
166
189
|
"-p",
|
|
@@ -215,12 +238,12 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
215
238
|
"--loudness-range-target",
|
|
216
239
|
type=float,
|
|
217
240
|
help=textwrap.dedent(
|
|
218
|
-
"""\
|
|
219
|
-
EBU Loudness Range Target in LUFS (default:
|
|
241
|
+
f"""\
|
|
242
|
+
EBU Loudness Range Target in LUFS (default: {FFmpegNormalize.DEFAULTS["loudness_range_target"]}).
|
|
220
243
|
Range is 1.0 - 50.0.
|
|
221
244
|
"""
|
|
222
245
|
),
|
|
223
|
-
default=
|
|
246
|
+
default=FFmpegNormalize.DEFAULTS["loudness_range_target"],
|
|
224
247
|
)
|
|
225
248
|
|
|
226
249
|
group_ebu.add_argument(
|
|
@@ -249,26 +272,26 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
249
272
|
"--true-peak",
|
|
250
273
|
type=float,
|
|
251
274
|
help=textwrap.dedent(
|
|
252
|
-
"""\
|
|
253
|
-
EBU Maximum True Peak in dBTP (default:
|
|
275
|
+
f"""\
|
|
276
|
+
EBU Maximum True Peak in dBTP (default: {FFmpegNormalize.DEFAULTS["true_peak"]}).
|
|
254
277
|
Range is -9.0 - +0.0.
|
|
255
278
|
"""
|
|
256
279
|
),
|
|
257
|
-
default
|
|
280
|
+
default=FFmpegNormalize.DEFAULTS["true_peak"],
|
|
258
281
|
)
|
|
259
282
|
|
|
260
283
|
group_ebu.add_argument(
|
|
261
284
|
"--offset",
|
|
262
285
|
type=float,
|
|
263
286
|
help=textwrap.dedent(
|
|
264
|
-
"""\
|
|
265
|
-
EBU Offset Gain (default:
|
|
287
|
+
f"""\
|
|
288
|
+
EBU Offset Gain (default: {FFmpegNormalize.DEFAULTS["offset"]}).
|
|
266
289
|
The gain is applied before the true-peak limiter in the first pass only.
|
|
267
290
|
The offset for the second pass will be automatically determined based on the first pass statistics.
|
|
268
291
|
Range is -99.0 - +99.0.
|
|
269
292
|
"""
|
|
270
293
|
),
|
|
271
|
-
default=
|
|
294
|
+
default=FFmpegNormalize.DEFAULTS["offset"],
|
|
272
295
|
)
|
|
273
296
|
|
|
274
297
|
group_ebu.add_argument(
|
|
@@ -471,7 +494,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
471
494
|
Will attempt to copy video codec by default.
|
|
472
495
|
"""
|
|
473
496
|
),
|
|
474
|
-
default="
|
|
497
|
+
default=FFmpegNormalize.DEFAULTS["video_codec"],
|
|
475
498
|
)
|
|
476
499
|
group_vcodec.add_argument(
|
|
477
500
|
"-sn",
|
|
@@ -565,7 +588,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
565
588
|
specified. (Default: `mkv`)
|
|
566
589
|
"""
|
|
567
590
|
),
|
|
568
|
-
default="
|
|
591
|
+
default=FFmpegNormalize.DEFAULTS["extension"],
|
|
569
592
|
)
|
|
570
593
|
return parser
|
|
571
594
|
|
|
@@ -581,6 +604,28 @@ def main() -> None:
|
|
|
581
604
|
_logger.error(message)
|
|
582
605
|
sys.exit(1)
|
|
583
606
|
|
|
607
|
+
# Handle --list-presets
|
|
608
|
+
preset_manager = PresetManager()
|
|
609
|
+
if cli_args.list_presets:
|
|
610
|
+
presets = preset_manager.get_available_presets()
|
|
611
|
+
if presets:
|
|
612
|
+
print("Available presets:")
|
|
613
|
+
for preset in presets:
|
|
614
|
+
print(f" - {preset}")
|
|
615
|
+
else:
|
|
616
|
+
print(f"No presets found in {preset_manager.presets_dir}")
|
|
617
|
+
sys.exit(0)
|
|
618
|
+
|
|
619
|
+
# Load and apply preset if specified
|
|
620
|
+
if cli_args.preset:
|
|
621
|
+
try:
|
|
622
|
+
preset_data = preset_manager.load_preset(cli_args.preset)
|
|
623
|
+
_logger.debug(f"Loaded preset '{cli_args.preset}': {preset_data}")
|
|
624
|
+
preset_manager.merge_preset_with_args(preset_data, cli_args)
|
|
625
|
+
_logger.info(f"Applied preset '{cli_args.preset}'")
|
|
626
|
+
except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
|
|
627
|
+
error(str(e))
|
|
628
|
+
|
|
584
629
|
def _split_options(opts: str) -> list[str]:
|
|
585
630
|
"""
|
|
586
631
|
Parse extra options (input or output) into a list.
|
|
@@ -93,6 +93,48 @@ class FFmpegNormalize:
|
|
|
93
93
|
FFmpegNormalizeError: If the ffmpeg executable is not found or does not support the loudnorm filter.
|
|
94
94
|
"""
|
|
95
95
|
|
|
96
|
+
# Default parameter values - single source of truth for all defaults
|
|
97
|
+
# Note: output_folder is a CLI-level option and not passed to FFmpegNormalize.__init__
|
|
98
|
+
DEFAULTS = {
|
|
99
|
+
"normalization_type": "ebu",
|
|
100
|
+
"target_level": -23.0,
|
|
101
|
+
"print_stats": False,
|
|
102
|
+
"loudness_range_target": 7.0,
|
|
103
|
+
"keep_loudness_range_target": False,
|
|
104
|
+
"keep_lra_above_loudness_range_target": False,
|
|
105
|
+
"true_peak": -2.0,
|
|
106
|
+
"offset": 0.0,
|
|
107
|
+
"lower_only": False,
|
|
108
|
+
"auto_lower_loudness_target": False,
|
|
109
|
+
"dual_mono": False,
|
|
110
|
+
"dynamic": False,
|
|
111
|
+
"audio_codec": "pcm_s16le",
|
|
112
|
+
"audio_bitrate": None,
|
|
113
|
+
"sample_rate": None,
|
|
114
|
+
"audio_channels": None,
|
|
115
|
+
"keep_original_audio": False,
|
|
116
|
+
"pre_filter": None,
|
|
117
|
+
"post_filter": None,
|
|
118
|
+
"video_codec": "copy",
|
|
119
|
+
"video_disable": False,
|
|
120
|
+
"subtitle_disable": False,
|
|
121
|
+
"metadata_disable": False,
|
|
122
|
+
"chapters_disable": False,
|
|
123
|
+
"extra_input_options": None,
|
|
124
|
+
"extra_output_options": None,
|
|
125
|
+
"output_format": None,
|
|
126
|
+
"output_folder": "normalized",
|
|
127
|
+
"extension": "mkv",
|
|
128
|
+
"dry_run": False,
|
|
129
|
+
"debug": False,
|
|
130
|
+
"progress": False,
|
|
131
|
+
"replaygain": False,
|
|
132
|
+
"batch": False,
|
|
133
|
+
"audio_streams": None,
|
|
134
|
+
"audio_default_only": False,
|
|
135
|
+
"keep_other_audio": False,
|
|
136
|
+
}
|
|
137
|
+
|
|
96
138
|
def __init__(
|
|
97
139
|
self,
|
|
98
140
|
normalization_type: Literal["ebu", "rms", "peak"] = "ebu",
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Preset management for ffmpeg-normalize."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ._ffmpeg_normalize import FFmpegNormalize
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_config_dir() -> Path:
|
|
18
|
+
"""Get the platform-specific config directory for ffmpeg-normalize.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Path: Configuration directory for presets
|
|
22
|
+
|
|
23
|
+
On Linux/macOS:
|
|
24
|
+
- XDG_CONFIG_HOME/ffmpeg-normalize (if XDG_CONFIG_HOME is set)
|
|
25
|
+
- ~/.config/ffmpeg-normalize (otherwise)
|
|
26
|
+
|
|
27
|
+
On Windows:
|
|
28
|
+
- %APPDATA%/ffmpeg-normalize
|
|
29
|
+
"""
|
|
30
|
+
if os.name == "nt": # Windows
|
|
31
|
+
appdata = os.getenv("APPDATA")
|
|
32
|
+
if appdata:
|
|
33
|
+
config_dir = Path(appdata) / "ffmpeg-normalize"
|
|
34
|
+
else:
|
|
35
|
+
config_dir = Path.home() / "AppData" / "Roaming" / "ffmpeg-normalize"
|
|
36
|
+
else: # Linux/macOS
|
|
37
|
+
xdg_config = os.getenv("XDG_CONFIG_HOME")
|
|
38
|
+
if xdg_config:
|
|
39
|
+
config_dir = Path(xdg_config) / "ffmpeg-normalize"
|
|
40
|
+
else:
|
|
41
|
+
config_dir = Path.home() / ".config" / "ffmpeg-normalize"
|
|
42
|
+
|
|
43
|
+
return config_dir
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_presets_dir() -> Path:
|
|
47
|
+
"""Get the presets directory.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Path: Directory containing preset files
|
|
51
|
+
"""
|
|
52
|
+
return get_config_dir() / "presets"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_default_presets_dir() -> Path:
|
|
56
|
+
"""Get the package default presets directory.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Path: Directory containing default preset files bundled with the package
|
|
60
|
+
"""
|
|
61
|
+
# Get the directory where this module is located
|
|
62
|
+
module_dir = Path(__file__).parent
|
|
63
|
+
return module_dir / "data" / "presets"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PresetManager:
|
|
67
|
+
"""Manages loading and merging of presets with CLI arguments."""
|
|
68
|
+
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
"""Initialize the preset manager."""
|
|
71
|
+
self.presets_dir = get_presets_dir()
|
|
72
|
+
self.default_presets_dir = get_default_presets_dir()
|
|
73
|
+
|
|
74
|
+
def get_available_presets(self) -> list[str]:
|
|
75
|
+
"""Get list of available preset names.
|
|
76
|
+
|
|
77
|
+
Includes both user-installed presets and default presets from the package.
|
|
78
|
+
User presets take precedence if they have the same name.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
list[str]: List of available preset names (without .json extension)
|
|
82
|
+
"""
|
|
83
|
+
presets = set()
|
|
84
|
+
|
|
85
|
+
# Get presets from user config directory
|
|
86
|
+
if self.presets_dir.exists():
|
|
87
|
+
for file in self.presets_dir.glob("*.json"):
|
|
88
|
+
presets.add(file.stem)
|
|
89
|
+
|
|
90
|
+
# Get default presets from package
|
|
91
|
+
if self.default_presets_dir.exists():
|
|
92
|
+
for file in self.default_presets_dir.glob("*.json"):
|
|
93
|
+
presets.add(file.stem)
|
|
94
|
+
|
|
95
|
+
return sorted(presets)
|
|
96
|
+
|
|
97
|
+
def load_preset(self, preset_name: str) -> dict[str, Any]:
|
|
98
|
+
"""Load a preset file by name.
|
|
99
|
+
|
|
100
|
+
Checks user config directory first, then falls back to package defaults.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
preset_name: Name of the preset (without .json extension)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
dict[str, Any]: Preset configuration as a dictionary
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
FileNotFoundError: If preset file doesn't exist
|
|
110
|
+
json.JSONDecodeError: If preset file is invalid JSON
|
|
111
|
+
"""
|
|
112
|
+
# Try user config directory first
|
|
113
|
+
preset_path = self.presets_dir / f"{preset_name}.json"
|
|
114
|
+
|
|
115
|
+
# Fall back to package default presets
|
|
116
|
+
if not preset_path.exists():
|
|
117
|
+
preset_path = self.default_presets_dir / f"{preset_name}.json"
|
|
118
|
+
|
|
119
|
+
if not preset_path.exists():
|
|
120
|
+
raise FileNotFoundError(
|
|
121
|
+
f"Preset '{preset_name}' not found. "
|
|
122
|
+
f"Available presets: {', '.join(self.get_available_presets()) or 'none'}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
with open(preset_path, "r") as f:
|
|
127
|
+
preset_data = json.load(f)
|
|
128
|
+
except json.JSONDecodeError as e:
|
|
129
|
+
raise json.JSONDecodeError(
|
|
130
|
+
f"Invalid JSON in preset '{preset_name}': {e.msg}",
|
|
131
|
+
e.doc,
|
|
132
|
+
e.pos,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if not isinstance(preset_data, dict):
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"Preset must be a JSON object, got {type(preset_data).__name__}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return preset_data
|
|
141
|
+
|
|
142
|
+
def merge_preset_with_args(
|
|
143
|
+
self, preset_data: dict[str, Any], cli_args: Any
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Merge preset data with CLI arguments, giving precedence to CLI args.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
preset_data: Dictionary of preset configuration
|
|
149
|
+
cli_args: Parsed CLI arguments (argparse Namespace object)
|
|
150
|
+
|
|
151
|
+
The CLI arguments take precedence over preset values. This function modifies
|
|
152
|
+
cli_args in place by setting attributes that were not explicitly provided
|
|
153
|
+
on the command line.
|
|
154
|
+
"""
|
|
155
|
+
for key, value in preset_data.items():
|
|
156
|
+
# Convert hyphens to underscores to match argparse attribute names
|
|
157
|
+
attr_name = key.replace("-", "_")
|
|
158
|
+
|
|
159
|
+
# Check if this attribute exists in cli_args
|
|
160
|
+
if not hasattr(cli_args, attr_name):
|
|
161
|
+
_logger.warning(
|
|
162
|
+
f"Preset option '{key}' is not a valid ffmpeg-normalize option. Skipping."
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Get the current value
|
|
167
|
+
current_value = getattr(cli_args, attr_name)
|
|
168
|
+
|
|
169
|
+
# Check if this was explicitly set by the user
|
|
170
|
+
# For most types, we can infer this by checking against the defaults:
|
|
171
|
+
# - None values were not explicitly set
|
|
172
|
+
# - Empty lists were not explicitly set
|
|
173
|
+
# - False boolean flags were not explicitly set
|
|
174
|
+
# - Default numeric values (specific to each option) need special handling
|
|
175
|
+
|
|
176
|
+
should_apply = False
|
|
177
|
+
|
|
178
|
+
if isinstance(value, bool):
|
|
179
|
+
# For boolean flags, apply preset only if currently False (not set)
|
|
180
|
+
should_apply = not current_value
|
|
181
|
+
elif isinstance(current_value, list):
|
|
182
|
+
# For lists (like output), apply preset only if empty
|
|
183
|
+
should_apply = not current_value
|
|
184
|
+
elif current_value is None:
|
|
185
|
+
# For None values, always apply preset (not explicitly set)
|
|
186
|
+
should_apply = True
|
|
187
|
+
else:
|
|
188
|
+
# For other values (numbers, strings), check if they match known defaults
|
|
189
|
+
# This is conservative: only override if the value is a known default
|
|
190
|
+
if (
|
|
191
|
+
attr_name in FFmpegNormalize.DEFAULTS
|
|
192
|
+
and current_value == FFmpegNormalize.DEFAULTS[attr_name]
|
|
193
|
+
):
|
|
194
|
+
should_apply = True
|
|
195
|
+
|
|
196
|
+
if should_apply:
|
|
197
|
+
setattr(cli_args, attr_name, value)
|
|
198
|
+
_logger.debug(f"Applied preset option '{key}' = {value}")
|
|
199
|
+
|
|
200
|
+
def validate_preset(self, preset_name: str) -> tuple[bool, str]:
|
|
201
|
+
"""Validate that a preset file exists and is valid.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
preset_name: Name of the preset to validate
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
tuple[bool, str]: (is_valid, message)
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
self.load_preset(preset_name)
|
|
211
|
+
return True, f"Preset '{preset_name}' is valid"
|
|
212
|
+
except FileNotFoundError as e:
|
|
213
|
+
return False, str(e)
|
|
214
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
215
|
+
return False, f"Error loading preset '{preset_name}': {e}"
|
|
216
|
+
|
|
217
|
+
def install_default_presets(self, force: bool = False) -> tuple[bool, str]:
|
|
218
|
+
"""Install default presets to user config directory.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
force: If True, overwrite existing presets
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
tuple[bool, str]: (success, message)
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
# Create presets directory if it doesn't exist
|
|
228
|
+
self.presets_dir.mkdir(parents=True, exist_ok=True)
|
|
229
|
+
|
|
230
|
+
if not self.default_presets_dir.exists():
|
|
231
|
+
return (
|
|
232
|
+
False,
|
|
233
|
+
f"Default presets directory not found at {self.default_presets_dir}",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Copy all default presets
|
|
237
|
+
installed = []
|
|
238
|
+
skipped = []
|
|
239
|
+
|
|
240
|
+
for preset_file in self.default_presets_dir.glob("*.json"):
|
|
241
|
+
dest_file = self.presets_dir / preset_file.name
|
|
242
|
+
|
|
243
|
+
if dest_file.exists() and not force:
|
|
244
|
+
skipped.append(preset_file.name)
|
|
245
|
+
else:
|
|
246
|
+
shutil.copy2(preset_file, dest_file)
|
|
247
|
+
installed.append(preset_file.name)
|
|
248
|
+
|
|
249
|
+
message = f"Installed {len(installed)} preset(s) to {self.presets_dir}"
|
|
250
|
+
if skipped:
|
|
251
|
+
message += f" ({len(skipped)} skipped, use --force to overwrite)"
|
|
252
|
+
|
|
253
|
+
return True, message
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
return False, f"Error installing default presets: {e}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ffmpeg-normalize
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.36.0
|
|
4
4
|
Summary: Normalize audio via ffmpeg
|
|
5
5
|
Keywords: ffmpeg,normalize,audio
|
|
6
6
|
Author: Werner Robitza
|
|
@@ -58,6 +58,16 @@ This program normalizes media files to a certain loudness level using the EBU R1
|
|
|
58
58
|
|
|
59
59
|
## 🆕 What's New
|
|
60
60
|
|
|
61
|
+
- Version 1.36.0 introduces **presets** with `--preset`! Save and reuse your favorite normalization configurations for different use cases. Comes with three built-in presets: `podcast` (AES standard), `music` (RMS-based batch normalization), and `streaming-video` (video content). Create custom presets too!
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
ffmpeg-normalize input.mp3 --preset podcast
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
applies the podcast preset (EBU R128, -16 LUFS) to your file. Learn more in the [presets guide](https://slhck.info/ffmpeg-normalize/usage/presets/).
|
|
70
|
+
|
|
61
71
|
- Version 1.35.0 has **batch/album normalization** with `--batch`. It preserves relative loudness between files! Perfect for music albums where you want to shift all tracks by the same amount.
|
|
62
72
|
|
|
63
73
|
Example:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ffmpeg_normalize/__init__.py,sha256=l0arjiMMBNbiH3IH67gT6SdZjPGAVLAdorUx38dNtvE,508
|
|
2
|
+
ffmpeg_normalize/__main__.py,sha256=4LgOHFwZS0lVcdYptNX4CCPDtG16yfMgUBjgom74FWg,25830
|
|
3
|
+
ffmpeg_normalize/_cmd_utils.py,sha256=1JspVpguAPsq7DqvyvjUNzHhVv8J3X93xNOMwito_jY,5284
|
|
4
|
+
ffmpeg_normalize/_errors.py,sha256=brTQ4osJ4fTA8wnyMPVVYfGwJ0wqeShRFydTEwi_VEY,48
|
|
5
|
+
ffmpeg_normalize/_ffmpeg_normalize.py,sha256=jCiYhXeV3u8e7sJKOOnGYN5X7stcLv9eg_h_pR-1olM,21372
|
|
6
|
+
ffmpeg_normalize/_logger.py,sha256=3Ap4Fxg7xGrzz7h4IGuNEf0KKstx0Rq_eLbHPrHzcrI,1841
|
|
7
|
+
ffmpeg_normalize/_media_file.py,sha256=awznS5C8ph6Mjy5dzwT-ubBpB3MXwbO94QFcn7mBejY,32197
|
|
8
|
+
ffmpeg_normalize/_presets.py,sha256=AffqFzAHvy0GxCuJl_qd3xknncPJUQx7OXbSfYAw2xg,8852
|
|
9
|
+
ffmpeg_normalize/_streams.py,sha256=4Dnzuunhqz2qsOhlDv0dKML-lLjmPmUmM7M4dpn66Ow,24910
|
|
10
|
+
ffmpeg_normalize/data/presets/music.json,sha256=rKtIKAD7jlMvnGRsx6QAyPHvrGu-Ecq4M5OcZgwNphA,96
|
|
11
|
+
ffmpeg_normalize/data/presets/podcast.json,sha256=spMBZ_tbfU81NF6o7ZUMyQmQ7kPz3jJ1GKH-hcRmt4s,132
|
|
12
|
+
ffmpeg_normalize/data/presets/streaming-video.json,sha256=Uy4QR0kSla4vXEqctZj26-8f_eS4SQxQWQ9zI_x5QBw,132
|
|
13
|
+
ffmpeg_normalize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
ffmpeg_normalize-1.36.0.dist-info/licenses/LICENSE.md,sha256=ig-_YggmJGbPQC_gUgBNFa0_XMsuHTpocivFnlOF4tE,1082
|
|
15
|
+
ffmpeg_normalize-1.36.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
16
|
+
ffmpeg_normalize-1.36.0.dist-info/entry_points.txt,sha256=1bdrW7-kJRc8tctjnGcfe_Fwx39z5JOm0yZnJHnmwl8,69
|
|
17
|
+
ffmpeg_normalize-1.36.0.dist-info/METADATA,sha256=hXTuiFMbjXW1T2R9n9PPs8n9lF4i3JQfNbIUhPSbRpE,13933
|
|
18
|
+
ffmpeg_normalize-1.36.0.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
ffmpeg_normalize/__init__.py,sha256=l0arjiMMBNbiH3IH67gT6SdZjPGAVLAdorUx38dNtvE,508
|
|
2
|
-
ffmpeg_normalize/__main__.py,sha256=ooM9MZRI4BfyF2G8eioY5KcumZ3jELRBkGNNchmF_ck,23788
|
|
3
|
-
ffmpeg_normalize/_cmd_utils.py,sha256=1JspVpguAPsq7DqvyvjUNzHhVv8J3X93xNOMwito_jY,5284
|
|
4
|
-
ffmpeg_normalize/_errors.py,sha256=brTQ4osJ4fTA8wnyMPVVYfGwJ0wqeShRFydTEwi_VEY,48
|
|
5
|
-
ffmpeg_normalize/_ffmpeg_normalize.py,sha256=NvFhiiYHLJz8M78WGEIit4Qbe-XsjLlrXvzKuNMW7v8,19973
|
|
6
|
-
ffmpeg_normalize/_logger.py,sha256=3Ap4Fxg7xGrzz7h4IGuNEf0KKstx0Rq_eLbHPrHzcrI,1841
|
|
7
|
-
ffmpeg_normalize/_media_file.py,sha256=awznS5C8ph6Mjy5dzwT-ubBpB3MXwbO94QFcn7mBejY,32197
|
|
8
|
-
ffmpeg_normalize/_streams.py,sha256=4Dnzuunhqz2qsOhlDv0dKML-lLjmPmUmM7M4dpn66Ow,24910
|
|
9
|
-
ffmpeg_normalize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
ffmpeg_normalize-1.35.0.dist-info/licenses/LICENSE.md,sha256=ig-_YggmJGbPQC_gUgBNFa0_XMsuHTpocivFnlOF4tE,1082
|
|
11
|
-
ffmpeg_normalize-1.35.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
12
|
-
ffmpeg_normalize-1.35.0.dist-info/entry_points.txt,sha256=1bdrW7-kJRc8tctjnGcfe_Fwx39z5JOm0yZnJHnmwl8,69
|
|
13
|
-
ffmpeg_normalize-1.35.0.dist-info/METADATA,sha256=Z4VJQhRECSX51YKaJFQkgY5CqaPxlgd6GIlyNYRFmSU,13387
|
|
14
|
-
ffmpeg_normalize-1.35.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|