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 +28 -0
- muxtools/__main__.py +4 -0
- muxtools/audio/__init__.py +6 -0
- muxtools/audio/audioutils.py +216 -0
- muxtools/audio/encoders.py +329 -0
- muxtools/audio/extractors.py +355 -0
- muxtools/audio/tools.py +42 -0
- muxtools/cli.py +198 -0
- muxtools/functions.py +87 -0
- muxtools/main.py +109 -0
- muxtools/misc/__init__.py +3 -0
- muxtools/misc/chapters.py +199 -0
- muxtools/muxing/__init__.py +7 -0
- muxtools/muxing/mux.py +110 -0
- muxtools/muxing/muxfiles.py +119 -0
- muxtools/muxing/tmdb.py +168 -0
- muxtools/muxing/tracks.py +186 -0
- muxtools/subtitle/__init__.py +7 -0
- muxtools/subtitle/font.py +52 -0
- muxtools/subtitle/styles.py +123 -0
- muxtools/subtitle/sub.py +465 -0
- muxtools/subtitle/subutils.py +48 -0
- muxtools/utils/__init__.py +10 -0
- muxtools/utils/convert.py +111 -0
- muxtools/utils/download.py +127 -0
- muxtools/utils/env.py +53 -0
- muxtools/utils/files.py +168 -0
- muxtools/utils/format.py +64 -0
- muxtools/utils/glob.py +44 -0
- muxtools/utils/log.py +59 -0
- muxtools/utils/parsing.py +216 -0
- muxtools/utils/types.py +116 -0
- muxtools-0.0.1.dist-info/LICENSE +373 -0
- muxtools-0.0.1.dist-info/METADATA +34 -0
- muxtools-0.0.1.dist-info/RECORD +38 -0
- muxtools-0.0.1.dist-info/WHEEL +5 -0
- muxtools-0.0.1.dist-info/entry_points.txt +2 -0
- muxtools-0.0.1.dist-info/top_level.txt +1 -0
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,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
|