muxtools 0.0.1__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.
muxtools/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ from .utils import *
2
+ from .muxing import *
3
+ from .audio import *
4
+ from .subtitle import *
5
+ from .misc import *
6
+
7
+ from . import main
8
+ from . import functions
9
+ from .main import *
10
+ from .functions import *
11
+
12
+
13
+ def entry_point():
14
+ import sys
15
+ from .cli import install_libraries, install_dependencies
16
+
17
+ if sys.argv:
18
+ if sys.argv[-1].lower() in ["libs", "libraries"]:
19
+ install_libraries()
20
+ sys.exit(0)
21
+ elif sys.argv[-1].lower() in ["install", "deps", "dependencies"]:
22
+ install_dependencies()
23
+ sys.exit(0)
24
+
25
+ error(
26
+ "No arguments passed.\nYou can use [b]libs[/] or [b]libraries[/] to install/update libraries of qaac and eac3to."
27
+ + "\nYou can use [b]install[/], [b]deps[/] or [b]dependencies[/] to install all sorts of executables."
28
+ )
muxtools/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from . import entry_point
2
+
3
+ if __name__ == "__main__":
4
+ entry_point()
@@ -0,0 +1,6 @@
1
+ from . import encoders, extractors, tools
2
+
3
+ from .audioutils import *
4
+ from .encoders import *
5
+ from .extractors import *
6
+ from .tools import *
@@ -0,0 +1,216 @@
1
+ import os
2
+ import re
3
+ import subprocess
4
+ from shutil import rmtree
5
+ from pymediainfo import Track
6
+ from functools import cmp_to_key
7
+
8
+
9
+ from ..utils.log import warn, error
10
+ from ..muxing.muxfiles import AudioFile
11
+ from ..utils.env import get_temp_workdir
12
+ from ..utils.download import get_executable
13
+ from ..utils.types import DitherType, Trim, AudioFormat
14
+
15
+ __all__ = ["ensure_valid_in", "sanitize_trims", "format_from_track", "is_fancy_codec", "has_libFLAC", "has_libFDK"]
16
+
17
+
18
+ def ensure_valid_in(
19
+ input: AudioFile,
20
+ supports_pipe: bool = True,
21
+ dither: bool = True,
22
+ dither_type: DitherType = DitherType.TRIANGULAR,
23
+ caller: any = None,
24
+ ) -> AudioFile | subprocess.Popen:
25
+ """
26
+ Ensures valid input for any encoder that accepts flac (all of them).
27
+ Passes existing file if no need to dither and is either wav or flac.
28
+ """
29
+ from .encoders import FF_FLAC
30
+
31
+ def getflac(input: AudioFile):
32
+ if supports_pipe:
33
+ return FF_FLAC(dither=dither, dither_type=dither_type).get_pipe(input)
34
+ else:
35
+ return FF_FLAC(
36
+ compression_level=0, dither=dither, dither_type=dither_type, output=os.path.join(get_temp_workdir(), "tempflac")
37
+ ).encode_audio(input, temp=True)
38
+
39
+ if input.has_multiple_tracks(caller):
40
+ msg = f"'{input.name}' is a container with multiple tracks.\n"
41
+ msg += f"The first audio track will be {'piped' if supports_pipe else 'extracted'} using default ffmpeg."
42
+ warn(msg, caller, 5)
43
+
44
+ minfo = input.get_mediainfo()
45
+ if is_fancy_codec(minfo):
46
+ warn("Encoding tracks with special DTS Features or Atmos is very much discouraged.", caller, 10)
47
+ form = minfo.format.lower()
48
+ if "wav" in form or "flac" in form or "pcm" in form:
49
+ if minfo.bit_depth > 16 and dither:
50
+ return getflac(input)
51
+ return input
52
+ else:
53
+ if input.is_lossy():
54
+ warn(f"It's strongly recommended to not reencode lossy audio! ({minfo.format})", caller, 5)
55
+ return getflac(input)
56
+
57
+
58
+ def compare_trims(trim: Trim, trim2: Trim):
59
+ if trim[0] is None and trim2[0] is not None:
60
+ return -1
61
+ elif trim[0] is not None and trim2[0] is None:
62
+ return 1
63
+ else:
64
+ if trim[0] is None and trim2[0] is None:
65
+ return trim[1] - trim2[1]
66
+ else:
67
+ return trim[0] - trim2[0]
68
+
69
+
70
+ def clean_trims(trims: list[Trim]):
71
+ sorted_trims = sorted(trims, key=cmp_to_key(compare_trims))
72
+
73
+ final_trims = []
74
+ for start, end in sorted_trims:
75
+ if final_trims:
76
+ prev_start, prev_end = final_trims[-1]
77
+ if start is None and prev_end is not None:
78
+ continue
79
+ elif prev_end is not None and prev_end >= start:
80
+ final_trims[-1] = (prev_start, max(prev_end, end))
81
+ elif prev_end is None and end is None:
82
+ continue
83
+ else:
84
+ final_trims.append((start, end))
85
+ elif start is not None or end is not None:
86
+ final_trims.append((start, end))
87
+
88
+ return final_trims
89
+
90
+
91
+ def sanitize_trims(
92
+ trims: Trim | list[Trim], total_frames: int = 0, uses_frames: bool = True, allow_negative_start: bool = False, caller: any = None
93
+ ) -> list[Trim]:
94
+ caller = caller if caller else sanitize_trims
95
+ if not isinstance(trims, (list, tuple)):
96
+ raise error("Trims must be a list of 2-tuples (or just one 2-tuple)", caller)
97
+ if not isinstance(trims, list):
98
+ trims = [trims]
99
+ for index, trim in enumerate(trims):
100
+ if not isinstance(trim, tuple):
101
+ raise error(f"The trim {trim} is not a tuple", caller)
102
+ if len(trim) != 2:
103
+ raise error(f"The trim {trim} needs 2 elements", caller)
104
+ for i in trim:
105
+ if not isinstance(i, (int, type(None))):
106
+ raise error(f"The trim {trim} must have 2 ints or None's", caller)
107
+ if trim[-1] == 0:
108
+ raise error("Slices cannot end with 0, if attempting to use an empty slice, use `None`", caller)
109
+
110
+ if trim[0] and trim[0] < 0 and not allow_negative_start:
111
+ raise error("The first part of a trim cannot be negative.", caller)
112
+
113
+ if trim[1] and uses_frames:
114
+ if total_frames and trim[1] > total_frames:
115
+ warn(f"The trim {trim} extends the frame number that was passed. Will be set to max frame.", caller, 5)
116
+ trims[index] = (trim[0], total_frames - 1)
117
+ if trim[1] < 0:
118
+ if not total_frames:
119
+ raise error("A trim cannot be negative if you're not passing the total frame number.", caller)
120
+ new_val = total_frames + trim[1]
121
+ trims[index] = (trim[0], new_val)
122
+ if new_val < 0:
123
+ raise error(f"The negative number of the trim {trim} is out of bounds.", caller)
124
+
125
+ return clean_trims(trims)
126
+
127
+
128
+ # Of course these are not all of the formats possible but those are the most common from what I know.
129
+ # fmt: off
130
+ formats = [
131
+ # Lossy
132
+ AudioFormat("AC-3", "ac3", "A_AC3"),
133
+ AudioFormat("E-AC-3", "eac3", "A_EAC3"),
134
+ AudioFormat("AAC*", "m4a", "A_AAC*"), # Lots of different AAC formats idk what they mean, don't care either
135
+ AudioFormat("Opus", "opus", "A_OPUS"),
136
+ AudioFormat("Vorbis", "ogg", "A_VORBIS"),
137
+ AudioFormat("/", "mp3", "mp4a-6B"), # MP3 has the format name split up into 3 variables so we're gonna ignore this
138
+
139
+ # Lossless
140
+ AudioFormat("FLAC", "flac", "A_FLAC", False),
141
+ AudioFormat("MLP FBA*", "thd", "A_TRUEHD", False), # Atmos stuff has some more shit in the format name
142
+ AudioFormat("PCM*", "wav", "A_PCM*", False),
143
+
144
+ # Disgusting DTS Stuff
145
+ AudioFormat("DTS XLL", "dtshd", "A_DTS", False), # Can be HD-MA or Headphone X or X, who the fuck knows
146
+ AudioFormat("DTS", "dts", "A_DTS"), # Can be lossy
147
+ ]
148
+ # fmt: on
149
+
150
+
151
+ def format_from_track(track: Track) -> AudioFormat | None:
152
+ for format in formats:
153
+ f = str(track.format)
154
+ if hasattr(track, "format_additionalfeatures") and track.format_additionalfeatures:
155
+ f = f"{f} {track.format_additionalfeatures}"
156
+ if "*" in format.format:
157
+ # matches = filter([f.lower()], format.format.lower())
158
+ if re.match(format.format.replace("*", ".*"), f, re.IGNORECASE):
159
+ return format
160
+ else:
161
+ if format.format.casefold() == f.casefold():
162
+ return format
163
+
164
+ if "*" in format.codecid:
165
+ # matches = filter([str(track.codec_id).lower()], format.codecid)
166
+ if re.match(format.codecid.replace("*", ".*"), str(track.codec_id), re.IGNORECASE):
167
+ return format
168
+ else:
169
+ if format.codecid.casefold() == str(track.codec_id).casefold():
170
+ return format
171
+ return None
172
+
173
+
174
+ def is_fancy_codec(track: Track) -> bool:
175
+ """
176
+ Tries to check if a track is some fancy DTS (X, Headphone X) or TrueHD with Atmos
177
+
178
+ :param track: Input track to check
179
+ """
180
+ codec_id = str(track.codec_id).casefold()
181
+ if codec_id == "A_TRUEHD".casefold():
182
+ # If it contains something other than "MLP FBA" it's probably atmos
183
+ return bool(re.sub("MLP FBA", "", str(track.format).strip(), re.IGNORECASE))
184
+ elif codec_id == "A_DTS".casefold():
185
+ # Not even lossless if this doesn't exist
186
+ if not hasattr(track, "format_additionalfeatures"):
187
+ return False
188
+ # If those additional features contain something after removing "XLL" its some fancy stuff
189
+ return bool(re.sub("XLL", "", str(track.format_additionalfeatures).strip(), re.IGNORECASE))
190
+
191
+ return False
192
+
193
+
194
+ def has_libFDK() -> bool:
195
+ """
196
+ Returns if whatever installation of ffmpeg being used has been compiled with libFDK
197
+ """
198
+ exe = get_executable("ffmpeg")
199
+ p = subprocess.run([exe, "-encoders"], capture_output=True, text=True)
200
+ for line in p.stderr.splitlines():
201
+ if "libfdk_aac" in line.lower():
202
+ return True
203
+ return False
204
+
205
+
206
+ def has_libFLAC() -> bool:
207
+ """
208
+ Returns if whatever installation of qaac being used has libFLAC
209
+ and as such can accept flac input
210
+ """
211
+ exe = get_executable("qaac")
212
+ p = subprocess.run([exe, "--check"], capture_output=True, text=True)
213
+ for line in p.stderr.splitlines():
214
+ if "libflac" in line.lower():
215
+ return True
216
+ return False
@@ -0,0 +1,329 @@
1
+ from shlex import split as splitcommand
2
+ from dataclasses import dataclass
3
+ import subprocess
4
+ import os
5
+
6
+
7
+ from .tools import Encoder
8
+ from ..muxing.muxfiles import AudioFile
9
+ from .audioutils import ensure_valid_in, has_libFDK, has_libFLAC
10
+ from ..utils.download import get_executable
11
+ from ..utils.log import warn, crit, debug, error
12
+ from ..utils.files import make_output, clean_temp_files
13
+ from ..utils.env import get_temp_workdir, run_commandline
14
+ from ..utils.types import DitherType, qAAC_MODE, PathLike
15
+
16
+ __all__ = ["FLAC", "FLACCL", "FF_FLAC", "Opus", "qAAC", "FDK_AAC"]
17
+
18
+
19
+ @dataclass
20
+ class FLAC(Encoder):
21
+ """
22
+ Uses the reference libFLAC encoder to encode audio to flac.
23
+
24
+ :param compression_level: Any int value from 0 to 8 (Higher = better but slower)
25
+ :param dither: Dithers any input down to 16 bit 48 khz if True
26
+ :param dither_type: FFMPEG dither_method used for dithering
27
+ :param append: Any other args one might pass to the encoder
28
+ :param output: Custom output. Can be a dir or a file.
29
+ Do not specify an extension unless you know what you're doing.
30
+ """
31
+
32
+ compression_level: int = 8
33
+ dither: bool = True
34
+ dither_type: DitherType = DitherType.TRIANGULAR
35
+ append: str = ""
36
+ output: PathLike | None = None
37
+
38
+ def encode_audio(self, input: AudioFile, quiet: bool = True, **kwargs) -> AudioFile:
39
+ if not isinstance(input, AudioFile):
40
+ input = AudioFile.from_file(input, self)
41
+ flaccl = get_executable("flac")
42
+ output = make_output(input.file, "flac", "libflac", self.output)
43
+ source = ensure_valid_in(input, dither=self.dither, dither_type=self.dither_type, caller=self, supports_pipe=False)
44
+ debug(f"Encoding '{input.file.stem}' to FLAC using libFLAC...", self)
45
+
46
+ args = [flaccl, f"-{self.compression_level}", str(source.file.resolve()) if isinstance(source, AudioFile) else "-"]
47
+ args.extend(["-o", str(output)])
48
+ if self.append:
49
+ args.extend(splitcommand(self.append))
50
+
51
+ stdin = subprocess.DEVNULL if isinstance(source, AudioFile) else source.stdout
52
+
53
+ if not run_commandline(args, quiet, False, stdin):
54
+ debug("Done", self)
55
+ clean_temp_files()
56
+ return AudioFile(output, input.container_delay, input.source)
57
+ else:
58
+ raise crit("Encoding to FLAC using libFLAC failed!", self)
59
+
60
+
61
+ @dataclass
62
+ class FLACCL(Encoder):
63
+ """
64
+ Uses the CUETools FLACCL encoder to encode audio to flac.
65
+ This one uses OpenCL or Cuda depending on your GPU and claims to have better compression than libFLAC.
66
+
67
+ :param compression_level: Any int value from 0 to 8 (Higher = better but slower)
68
+ :param dither: Dithers any input down to 16 bit 48 khz if True
69
+ :param dither_type: FFMPEG dither_method used for dithering
70
+ :param append: Any other args one might pass to the encoder
71
+ :param output: Custom output. Can be a dir or a file.
72
+ Do not specify an extension unless you know what you're doing.
73
+ """
74
+
75
+ compression_level: int = 8
76
+ dither: bool = True
77
+ dither_type: DitherType = DitherType.TRIANGULAR
78
+ append: str = ""
79
+ output: PathLike | None = None
80
+
81
+ def encode_audio(self, input: AudioFile, quiet: bool = True, **kwargs) -> AudioFile:
82
+ if not isinstance(input, AudioFile):
83
+ input = AudioFile.from_file(input, self)
84
+ flaccl = get_executable("CUETools.FLACCL.cmd")
85
+ output = make_output(input.file, "flac", "flaccl", self.output)
86
+ source = ensure_valid_in(input, dither=self.dither, dither_type=self.dither_type, caller=self, supports_pipe=False)
87
+ debug(f"Encoding '{input.file.stem}' to FLAC using FLACCL...", self)
88
+
89
+ args = [flaccl, f"-{self.compression_level}", str(source.file.resolve()) if isinstance(source, AudioFile) else "-"]
90
+ args.extend(["-o", str(output)])
91
+ if self.append:
92
+ args.extend(splitcommand(self.append))
93
+
94
+ stdin = subprocess.DEVNULL if isinstance(source, AudioFile) else source.stdout
95
+
96
+ if not run_commandline(args, quiet, False, stdin):
97
+ debug("Done", self)
98
+ clean_temp_files()
99
+ return AudioFile(output, input.container_delay, input.source)
100
+ else:
101
+ raise crit("Encoding to FLAC using FLACCL failed!", self)
102
+
103
+
104
+ @dataclass
105
+ class FF_FLAC(Encoder):
106
+ """
107
+ Uses the ffmpeg/libav FLAC encoder to encode audio to flac.
108
+
109
+ :param compression_level: Any int value from 0 to 12 (Higher = better but slower)
110
+ :param dither: Dithers any input down to 16 bit 48 khz if True
111
+ :param dither_type: FFMPEG dither_method used for dithering
112
+ :param append: Any other args one might pass to the encoder
113
+ :param output: Custom output. Can be a dir or a file.
114
+ Do not specify an extension unless you know what you're doing.
115
+ """
116
+
117
+ compression_level: int = 10
118
+ dither: bool = True
119
+ dither_type: DitherType = DitherType.TRIANGULAR
120
+ append: str = ""
121
+ output: PathLike | None = None
122
+
123
+ def _base_command(self, input: AudioFile, compression: int = 0) -> list[str]:
124
+ # fmt: off
125
+ args = [get_executable("ffmpeg"), "-hide_banner", "-i", str(input.file.resolve()), "-map", "0:a:0", "-c:a", "flac", "-compression_level", str(compression)]
126
+ if self.dither:
127
+ args.extend(['-sample_fmt', 's16', '-ar', '48000', '-resampler', 'soxr', '-precision', '24', '-dither_method', self.dither_type.name.lower()])
128
+ if self.append:
129
+ args.extend(splitcommand(self.append))
130
+ return args
131
+ # fmt: on
132
+
133
+ def encode_audio(self, input: AudioFile, quiet: bool = True, **kwargs) -> AudioFile:
134
+ if not isinstance(input, AudioFile):
135
+ input = AudioFile.from_file(input, self)
136
+ output = make_output(input.file, "flac", "ffmpeg", self.output)
137
+ if "temp" in kwargs.keys():
138
+ debug(f"Preparing audio for input to other encoder using ffmpeg...", self)
139
+ else:
140
+ debug(f"Encoding '{input.file.stem}' to FLAC using ffmpeg...", self)
141
+ args = self._base_command(input, self.compression_level)
142
+ args.append(str(output.resolve()))
143
+
144
+ if not run_commandline(args, quiet):
145
+ debug("Done", self)
146
+ return AudioFile(output, input.container_delay, input.source)
147
+ else:
148
+ raise crit("Encoding to flac using ffmpeg failed!", self)
149
+
150
+ def get_pipe(self, input: AudioFile) -> subprocess.Popen:
151
+ debug(f"Piping audio for input to other encoder using ffmpeg...", self)
152
+ args = self._base_command(input, 0)
153
+ args.extend(["-f", "flac", "-"])
154
+ p = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=False)
155
+ return p
156
+
157
+
158
+ @dataclass
159
+ class Opus(Encoder):
160
+ """
161
+ Uses opusenc to encode audio to opus.
162
+
163
+ :param bitrate: Any int value representing kbps from 1 to 512
164
+ Automatically chooses 192 and 320 for stereo and surround respectively if None
165
+
166
+ :param vbr: Uses VBR encoding if True
167
+ :param dither: Dithers any input down to 16 bit 48 khz if True, lets the encoder handle it if False
168
+ :param dither_type: FFMPEG dither_method used for dithering
169
+ :param append: Any other args one might pass to the encoder
170
+ :param output: Custom output. Can be a dir or a file.
171
+ Do not specify an extension unless you know what you're doing.
172
+ """
173
+
174
+ bitrate: int | None = None
175
+ vbr: bool = True
176
+ dither: bool = False
177
+ dither_type: DitherType = DitherType.TRIANGULAR
178
+ append: str = ""
179
+ output: PathLike | None = None
180
+
181
+ def encode_audio(self, input: AudioFile | PathLike, quiet: bool = True, **kwargs) -> AudioFile:
182
+ if not isinstance(input, AudioFile):
183
+ input = AudioFile.from_file(input, self)
184
+
185
+ exe = get_executable("opusenc")
186
+ if not self.bitrate:
187
+ info = input.get_mediainfo()
188
+ match (info.channel_s):
189
+ case _ if info.channel_s == 2:
190
+ self.bitrate = 192
191
+ case _ if info.channel_s > 6:
192
+ self.bitrate = 420
193
+ case _:
194
+ self.bitrate = 320
195
+ debug(f"Encoding '{input.file.stem}' to Opus ({self.bitrate} kbps) using opusenc...", self)
196
+ else:
197
+ debug(f"Encoding '{input.file.stem}' to Opus using opusenc...", self)
198
+
199
+ output = make_output(input.file, "opus", "opusenc", self.output)
200
+ source = ensure_valid_in(input, dither=self.dither, dither_type=self.dither_type, caller=self, supports_pipe=True)
201
+
202
+ args = [exe, "--vbr" if self.vbr else "--cvbr", "--bitrate", str(self.bitrate)]
203
+ if self.append:
204
+ args.extend(splitcommand(self.append))
205
+ args.append(str(source.file.resolve()) if isinstance(source, AudioFile) else "-")
206
+ args.append(str(output))
207
+
208
+ stdin = subprocess.DEVNULL if isinstance(source, AudioFile) else source.stdout
209
+
210
+ if not run_commandline(args, quiet, False, stdin):
211
+ debug("Done", self)
212
+ clean_temp_files()
213
+ return AudioFile(output, input.container_delay, input.source)
214
+ else:
215
+ raise crit("Encoding to opus using opusenc failed!", self)
216
+
217
+
218
+ @dataclass
219
+ class qAAC(Encoder):
220
+ """
221
+ Uses qAAC to encode audio to AAC.
222
+
223
+ :param q: Quality value ranging from 0 to 127 if using TVBR, otherwise bitrate in kbps
224
+ :param mode: Encoding mode, Defaults to TVBR
225
+ :param dither: Dithers any input down to 16 bit 48 khz if True, lets the encoder handle it if False
226
+ :param dither_type: FFMPEG dither_method used for dithering
227
+ :param append: Any other args one might pass to the encoder
228
+ Adds " --no-delay --no-optimize" by default to prevent desync with video
229
+ :param output: Custom output. Can be a dir or a file.
230
+ Do not specify an extension unless you know what you're doing.
231
+ """
232
+
233
+ q: int = 127
234
+ mode: qAAC_MODE | int = qAAC_MODE.TVBR
235
+ dither: bool = False
236
+ dither_type: DitherType = DitherType.TRIANGULAR
237
+ append: str = ""
238
+ output: PathLike | None = None
239
+
240
+ def encode_audio(self, input: AudioFile, quiet: bool = True, **kwargs) -> AudioFile:
241
+ if not isinstance(input, AudioFile):
242
+ input = AudioFile.from_file(input, self)
243
+ output = make_output(input.file, "aac", "qaac", self.output)
244
+ source = ensure_valid_in(input, dither=self.dither, dither_type=self.dither_type, caller=self, supports_pipe=False)
245
+ qaac = get_executable("qaac")
246
+
247
+ if not has_libFLAC():
248
+ raise error(
249
+ "Your installation of qaac does not have libFLAC.\nIt is needed for proper piping from ffmpeg etc."
250
+ + "\nYou can download it from https://github.com/xiph/flac/releases"
251
+ + "\nFor installation check https://github.com/nu774/qaac/wiki/Installation",
252
+ self,
253
+ )
254
+
255
+ debug(f"Encoding '{input.file.stem}' to AAC using qAAC...", self)
256
+ args = [qaac, "--no-delay", "--no-optimize", "--threading", f"--{self.mode.name.lower()}", str(self.q)]
257
+ if self.append:
258
+ args.extend(splitcommand(self.append))
259
+ args.extend(["-o", str(output), str(source.file.resolve()) if isinstance(source, AudioFile) else "-"])
260
+
261
+ stdin = subprocess.DEVNULL if isinstance(source, AudioFile) else source.stdout
262
+
263
+ if not run_commandline(args, quiet, False, stdin):
264
+ debug("Done", self)
265
+ clean_temp_files()
266
+ return AudioFile(output, input.container_delay, input.source)
267
+ else:
268
+ raise crit("Encoding to AAC using qAAC failed!", self)
269
+
270
+
271
+ @dataclass
272
+ class FDK_AAC(Encoder):
273
+ """
274
+ Uses the libFDK implementation in ffmpeg to encode audio to AAC.
275
+ It's strongly recommended to use qAAC if you're on windows because its straight up the best AAC encoder.
276
+
277
+ :param bitrate_mode: Any int value from 0 - 5
278
+ 0 will be CBR and using the bitrate below, 1 - 5 are true VBR modes
279
+ See https://wiki.hydrogenaud.io/index.php?title=Fraunhofer_FDK_AAC#Bitrate_Modes
280
+
281
+ :param bitrate: Any int value representing kbps
282
+ :param dither: Dithers any input down to 16 bit 48 khz if True, lets the encoder handle it if False
283
+ :param dither_type: FFMPEG dither_method used for dithering
284
+ :param append: Any other args one might pass to the encoder
285
+ :param output: Custom output. Can be a dir or a file.
286
+ Do not specify an extension unless you know what you're doing.
287
+ """
288
+
289
+ bitrate_mode: int = 5
290
+ bitrate: int = 256
291
+ dither: bool = False
292
+ dither_type: DitherType = DitherType.TRIANGULAR
293
+ append: str = ""
294
+ output: PathLike | None = None
295
+
296
+ def encode_audio(self, input: AudioFile, quiet: bool = True, **kwargs) -> AudioFile:
297
+ if not isinstance(input, AudioFile):
298
+ input = AudioFile.from_file(input, self)
299
+ output = make_output(input.file, "m4a", "fdkaac", self.output)
300
+ if not has_libFDK():
301
+ raise error(
302
+ "Your installation of ffmpeg wasn't compiled with libFDK."
303
+ + "\nYou can download builds with the non-free flag from https://github.com/AnimMouse/ffmpeg-autobuild/releases",
304
+ self,
305
+ )
306
+ if os.name == "nt":
307
+ warn("It is strongly recommended to use qAAC on windows. See docs.", self, 5)
308
+ debug(f"Encoding '{input.file.stem}' to AAC using libFDK...", self)
309
+ # fmt: off
310
+ args = [get_executable("ffmpeg"), "-hide_banner", "-i", str(input.file.resolve()), "-map", "0:a:0", "-c:a", "libfdk_aac"]
311
+ if self.bitrate_mode > 0:
312
+ args.extend(['-vbr', str(self.bitrate_mode)])
313
+ else:
314
+ args.extend(['-b:a', f'{self.bitrate}k'])
315
+ if self.dither:
316
+ args.extend(['-sample_fmt', 's16', '-ar', '48000', '-resampler', 'soxr', '-precision', '24', '-dither_method', self.dither_type.name.lower()])
317
+ if self.append:
318
+ args.extend(splitcommand(self.append))
319
+ args.append(str(output))
320
+ # fmt: on
321
+ if not run_commandline(args, quiet, False):
322
+ debug("Done", self)
323
+ clean_temp_files()
324
+ return AudioFile(output, input.container_delay, input.source)
325
+ else:
326
+ raise crit("Encoding to AAC using libFDK failed!", self)
327
+
328
+
329
+ # TODO: Implement the dolby shit