Last active
April 10, 2026 09:45
-
-
Save yuygfgg/8a2686b230b2919b1d59f88addd4b0f6 to your computer and use it in GitHub Desktop.
atmos_tools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import argparse | |
| import pathlib | |
| import dataclasses | |
| import subprocess | |
| import shutil | |
| import platform | |
| import os | |
| import sys | |
| SCRIPT_NAME = "Atmos Audio Decoder" | |
| SCRIPT_VERSION = "0.0.1" | |
| if platform.system() != "Darwin": | |
| raise RuntimeError("Error: This script is designed to run on macOS only.") | |
| _NAMES_2_0 = ["L", "R"] | |
| _NAMES_3_1 = ["L", "R", "C", "LFE"] | |
| _NAMES_5_1 = ["L", "R", "C", "LFE", "Ls", "Rs"] | |
| _NAMES_7_1 = _NAMES_5_1 + ["Lrs", "Rrs"] | |
| _NAMES_9_1 = _NAMES_7_1 + ["Lw", "Rw"] | |
| _HEIGHT_NAMES_2 = ["Ltm", "Rtm"] | |
| _HEIGHT_NAMES_4 = ["Ltf", "Rtf", "Ltr", "Rtr"] | |
| _HEIGHT_NAMES_6 = ["Ltf", "Rtf", "Ltm", "Rtm", "Ltr", "Rtr"] | |
| CHANNELS = { | |
| "2.0": {"id": 0, "names": _NAMES_2_0}, | |
| "3.1": {"id": 3, "names": _NAMES_3_1}, | |
| "5.1": {"id": 7, "names": _NAMES_5_1}, | |
| "7.1": {"id": 11, "names": _NAMES_7_1}, | |
| "9.1": {"id": 12, "names": _NAMES_9_1}, | |
| "5.1.2": {"id": 13, "names": _NAMES_5_1 + _HEIGHT_NAMES_2}, | |
| "5.1.4": {"id": 14, "names": _NAMES_5_1 + _HEIGHT_NAMES_4}, | |
| "7.1.2": {"id": 15, "names": _NAMES_7_1 + _HEIGHT_NAMES_2}, | |
| "7.1.4": {"id": 16, "names": _NAMES_7_1 + _HEIGHT_NAMES_4}, | |
| "7.1.6": {"id": 17, "names": _NAMES_7_1 + _HEIGHT_NAMES_6}, | |
| "9.1.2": {"id": 18, "names": _NAMES_9_1 + _HEIGHT_NAMES_2}, | |
| "9.1.4": {"id": 19, "names": _NAMES_9_1 + _HEIGHT_NAMES_4}, | |
| "9.1.6": {"id": 20, "names": _NAMES_9_1 + _HEIGHT_NAMES_6}, | |
| } | |
| @dataclasses.dataclass | |
| class Config: | |
| mode: str | |
| gst_exe_path: pathlib.Path | |
| wav_channels_config: str | None | |
| wav_no_numbers_in_filename: bool | |
| wav_single_thread_decode: bool | |
| cavernize_exe_path: pathlib.Path | None | |
| ffmpeg_exe_path: pathlib.Path | |
| atmos_fps: float | |
| atmos_tool_name: str | |
| atmos_tool_version: str | |
| class AtmosDecode: | |
| def __init__(self, config: Config) -> None: | |
| self.config: Config = config | |
| self.gst_exe_path: pathlib.Path | None = None | |
| self.custom_env = os.environ.copy() | |
| if self.config.mode in ["wav", "thd_atmos_files", "binaural"]: | |
| resolved_exe_path_str = shutil.which(str(self.config.gst_exe_path)) | |
| if resolved_exe_path_str: | |
| self.gst_exe_path = pathlib.Path(resolved_exe_path_str).resolve() | |
| else: | |
| direct_path = self.config.gst_exe_path.resolve() | |
| if direct_path.is_file(): | |
| self.gst_exe_path = direct_path | |
| else: | |
| raise RuntimeError( | |
| f"GStreamer executable '{self.config.gst_exe_path}' " | |
| f"not found in PATH or as a direct file path." | |
| ) | |
| self._setup_gstreamer_env() | |
| def _setup_gstreamer_env(self) -> None: | |
| app_root_dir = None | |
| gst_launch_resolved_path = self.gst_exe_path | |
| assert gst_launch_resolved_path | |
| candidate_app_root = None | |
| for parent_dir in gst_launch_resolved_path.parents: | |
| if parent_dir.name.endswith(".app"): | |
| candidate_app_root = parent_dir | |
| break | |
| if candidate_app_root: | |
| print(f"Info: Detected macOS .app bundle: {app_root_dir}") | |
| core_plugins_path = ( | |
| candidate_app_root | |
| / "Contents/Frameworks/GStreamer.framework/Versions/Current/Resources/lib/gstreamer-1.0/" | |
| ) | |
| dolby_plugins_path = candidate_app_root / "Contents/PlugIns/gst-plugins/" | |
| gst_plugin_path_parts = [] | |
| if core_plugins_path.is_dir(): | |
| gst_plugin_path_parts.append(str(core_plugins_path.resolve())) | |
| else: | |
| print( | |
| f"Warning: Core GStreamer plugins directory not found in .app bundle: {core_plugins_path}", | |
| file=sys.stderr, | |
| ) | |
| if dolby_plugins_path.is_dir(): | |
| gst_plugin_path_parts.append(str(dolby_plugins_path.resolve())) | |
| else: | |
| print( | |
| f"Warning: Dolby GStreamer plugins directory not found in .app bundle: {dolby_plugins_path}", | |
| file=sys.stderr, | |
| ) | |
| if gst_plugin_path_parts: | |
| combined_plugin_path = ":".join(gst_plugin_path_parts) | |
| existing_env_gst_plugin_path = self.custom_env.get("GST_PLUGIN_PATH") | |
| self.custom_env["GST_PLUGIN_PATH"] = ( | |
| f"{combined_plugin_path}:{existing_env_gst_plugin_path}" | |
| if existing_env_gst_plugin_path | |
| else combined_plugin_path | |
| ) | |
| print( | |
| f"Info: Automatically setting GST_PLUGIN_PATH for .app bundle: {self.custom_env['GST_PLUGIN_PATH']}" | |
| ) | |
| elif not (core_plugins_path.is_dir() and dolby_plugins_path.is_dir()): | |
| print( | |
| "Warning: Detected macOS .app bundle, but required GStreamer plugin subdirectories were not found.", | |
| file=sys.stderr, | |
| ) | |
| bin_dir = gst_launch_resolved_path.parent | |
| if bin_dir.name == "bin": | |
| resources_dir = bin_dir.parent | |
| if resources_dir.name == "Resources": | |
| scanner_executable_path = ( | |
| resources_dir / "libexec" / "gst-plugin-scanner" | |
| ) | |
| if scanner_executable_path.is_file() and os.access( | |
| scanner_executable_path, os.X_OK | |
| ): | |
| self.custom_env["GST_PLUGIN_SCANNER"] = str( | |
| scanner_executable_path.resolve() | |
| ) | |
| print( | |
| f"Info: Automatically setting GST_PLUGIN_SCANNER for .app bundle: {self.custom_env['GST_PLUGIN_SCANNER']}" | |
| ) | |
| else: | |
| print( | |
| f"Warning: gst-plugin-scanner not found or not executable at {scanner_executable_path}.", | |
| file=sys.stderr, | |
| ) | |
| else: | |
| print( | |
| f"Warning: Could not derive 'Resources' directory from {gst_launch_resolved_path}.", | |
| file=sys.stderr, | |
| ) | |
| else: | |
| print( | |
| f"Warning: gst-launch executable not in expected 'bin' subdirectory: {gst_launch_resolved_path}.", | |
| file=sys.stderr, | |
| ) | |
| else: | |
| self._setup_fallback_plugin_logic() | |
| def _setup_fallback_plugin_logic(self) -> None: | |
| print( | |
| "Info: Using default GStreamer plugin discovery. Ensure GStreamer and plugins are correctly installed/configured " | |
| "or set GST_PLUGIN_PATH/GST_PLUGIN_SCANNER manually if issues arise." | |
| ) | |
| def _prepare_file_path_for_cli(self, source_path: pathlib.Path) -> str: | |
| return str(source_path.resolve()) | |
| def _build_gstreamer_eac3_decode_cmd( | |
| self, | |
| input_file: pathlib.Path, | |
| out_file: pathlib.Path, | |
| channel_id: int, | |
| out_channel_config_id: int, | |
| ) -> list[str]: | |
| if not self.gst_exe_path: | |
| raise RuntimeError("GStreamer executable path not set.") | |
| return [ | |
| str(self.gst_exe_path), | |
| "filesrc", | |
| f"location={self._prepare_file_path_for_cli(input_file)}", | |
| "!", | |
| "dlbac3parse", | |
| "!", | |
| "dlbaudiodecbin", | |
| f"out-ch-config={out_channel_config_id}", | |
| "!", | |
| "deinterleave", | |
| "name=d", | |
| f"d.src_{channel_id}", | |
| "!", | |
| "wavenc", | |
| "!", | |
| "filesink", | |
| f"location={self._prepare_file_path_for_cli(out_file)}", | |
| ] | |
| def _build_gstreamer_truehd_decode_cmd( | |
| self, | |
| input_file: pathlib.Path, | |
| out_file: pathlib.Path, | |
| channel_id: int, | |
| out_channel_config_id: int, | |
| ) -> list[str]: | |
| if not self.gst_exe_path: | |
| raise RuntimeError("GStreamer executable path not set.") | |
| return [ | |
| str(self.gst_exe_path), | |
| "filesrc", | |
| f"location={self._prepare_file_path_for_cli(input_file)}", | |
| "!", | |
| "dlbtruehdparse", | |
| "align-major-sync=false", | |
| "!", | |
| "dlbaudiodecbin", | |
| "truehddec-presentation=16", | |
| f"out-ch-config={out_channel_config_id}", | |
| "!", | |
| "deinterleave", | |
| "name=d", | |
| f"d.src_{channel_id}", | |
| "!", | |
| "wavenc", | |
| "!", | |
| "filesink", | |
| f"location={self._prepare_file_path_for_cli(out_file)}", | |
| ] | |
| def _build_gstreamer_truehd_to_pcm_meta_cmd( | |
| self, | |
| input_file: pathlib.Path, | |
| out_pcm_file: pathlib.Path, | |
| out_metadata_file: pathlib.Path, | |
| ) -> list[str]: | |
| if not self.gst_exe_path: | |
| raise RuntimeError("GStreamer executable path not set.") | |
| return [ | |
| str(self.gst_exe_path), | |
| "filesrc", | |
| f"location={self._prepare_file_path_for_cli(input_file)}", | |
| "!", | |
| "dlbtruehdparse", | |
| "align-major-sync=false", | |
| "!", | |
| "dlbtruehddec", | |
| "out-ch-config=21", | |
| "presentation=16", | |
| "!", | |
| "tee", | |
| "name=t", | |
| "!", | |
| "queue", | |
| "!", | |
| "oamdcapsfeature", | |
| "remove=true", | |
| "!", | |
| "audioconvert", | |
| "!", | |
| "audio/x-raw,format=S24LE,layout=interleaved", | |
| "!", | |
| "filesink", | |
| f"location={self._prepare_file_path_for_cli(out_pcm_file)}", | |
| "t.", | |
| "!", | |
| "queue", | |
| "!", | |
| "oamdserialize", | |
| "!", | |
| "filesink", | |
| f"location={self._prepare_file_path_for_cli(out_metadata_file)}", | |
| ] | |
| def _build_cavernize_eac3_to_adm_cmd( | |
| self, input_file: pathlib.Path, out_adm_bwf_file: pathlib.Path | |
| ) -> list[str]: | |
| if not self.config.cavernize_exe_path: | |
| raise RuntimeError( | |
| "Path to CavernizeGUI.exe not specified (--cavernize_exe_path)." | |
| ) | |
| cavernize_resolved_path = self.config.cavernize_exe_path.resolve() | |
| if not cavernize_resolved_path.is_file(): | |
| raise RuntimeError( | |
| f"CavernizeGUI.exe not found at: {cavernize_resolved_path}" | |
| ) | |
| wine_exe_path = shutil.which("wine") | |
| if not wine_exe_path: | |
| raise RuntimeError( | |
| "Wine executable not found in PATH. Wine is required for CavernizeGUI on macOS." | |
| ) | |
| return [ | |
| wine_exe_path, | |
| str(cavernize_resolved_path), | |
| "-i", | |
| self._prepare_file_path_for_cli(input_file), | |
| "-f", | |
| "ADM_BWF_Atmos", | |
| "-o", | |
| self._prepare_file_path_for_cli(out_adm_bwf_file), | |
| ] | |
| def _build_gstreamer_truehd_binaural_cmd( | |
| self, input_file: pathlib.Path, out_file: pathlib.Path | |
| ) -> list[str]: | |
| if not self.gst_exe_path: | |
| raise RuntimeError("GStreamer executable path not set.") | |
| return [ | |
| str(self.gst_exe_path), | |
| "filesrc", | |
| f"location={self._prepare_file_path_for_cli(input_file)}", | |
| "!", | |
| "dlbtruehdparse", | |
| "align-major-sync=false", | |
| "!", | |
| "dlbtruehddec", | |
| "out-ch-config=21", | |
| "presentation=16", | |
| "!", | |
| "oamdcapsfeature", | |
| "float=true", | |
| "!", | |
| "dlbdapvr", | |
| "!", | |
| "wavenc", | |
| "!", | |
| "filesink", | |
| f"location={self._prepare_file_path_for_cli(out_file)}", | |
| ] | |
| def _build_gstreamer_eac3_binaural_cmd( | |
| self, input_file: pathlib.Path, out_file: pathlib.Path | |
| ) -> list[str]: | |
| if not self.gst_exe_path: | |
| raise RuntimeError("GStreamer executable path not set.") | |
| return [ | |
| str(self.gst_exe_path), | |
| "filesrc", | |
| f"location={self._prepare_file_path_for_cli(input_file)}", | |
| "!", | |
| "dlbac3parse", | |
| "!", | |
| "dlbac3dec", | |
| "out-ch-config=21", | |
| "!", | |
| "oamdcapsfeature", | |
| "float=true", | |
| "!", | |
| "dlbdapvr", | |
| "!", | |
| "wavenc", | |
| "!", | |
| "filesink", | |
| f"location={self._prepare_file_path_for_cli(out_file)}", | |
| ] | |
| def _generate_atmos_file_content( | |
| self, metadata_filename: str, caf_filename: str | |
| ) -> str: | |
| object_ids_str = "\n".join([f" - ID: {i}" for i in range(10, 25)]) | |
| return f""" | |
| version: 0.5.1 | |
| presentations: | |
| - type: home | |
| simplified: false | |
| metadata: {metadata_filename} | |
| audio: {caf_filename} | |
| offset: 0 | |
| fps: {self.config.atmos_fps} | |
| scBedConfiguration: [3] | |
| creationTool: {self.config.atmos_tool_name} | |
| creationToolVersion: {self.config.atmos_tool_version} | |
| downmixType_5to2: LoRo_Stereo | |
| 51-to-20_LsRs90degPhaseShift: false | |
| warpMode: warping | |
| bedInstances: | |
| - channels: | |
| - channel: LFE | |
| ID: 3 | |
| objects: | |
| {object_ids_str} | |
| """ | |
| def decode( | |
| self, input_file: pathlib.Path, output_base_path: pathlib.Path | None = None | |
| ): | |
| if not input_file.is_file(): | |
| raise RuntimeError( | |
| f"Input file {input_file.resolve()} is not a regular file." | |
| ) | |
| is_eac3, is_truehd = False, False | |
| try: | |
| with input_file.open("rb") as f: | |
| leading_bytes = f.read(10) | |
| if leading_bytes.startswith(b"\x0b\x77"): | |
| is_eac3 = True | |
| f.seek(0) | |
| content_sample = f.read(2048) | |
| if b"\xf8\x72\x6f\xba" in content_sample: | |
| is_truehd = True | |
| except Exception as e: | |
| print( | |
| f"Warning: Error reading input file for type detection: {e}. Proceeding.", | |
| file=sys.stderr, | |
| ) | |
| if not is_eac3 and not is_truehd: | |
| print( | |
| "Warning: Could not definitively identify input file type. Proceeding with selected mode.", | |
| file=sys.stderr, | |
| ) | |
| if self.config.mode == "wav": | |
| if not self.config.wav_channels_config: | |
| raise RuntimeError( | |
| "Channel configuration ('--wav_channels_config') is required for 'wav' mode." | |
| ) | |
| if not (is_eac3 or is_truehd): | |
| raise RuntimeError( | |
| "'wav' mode requires identifiable E-AC3 or TrueHD input." | |
| ) | |
| gst_cmd_builder = ( | |
| self._build_gstreamer_eac3_decode_cmd | |
| if is_eac3 | |
| else self._build_gstreamer_truehd_decode_cmd | |
| ) | |
| print( | |
| f"Info: Detected {'E-AC3' if is_eac3 else 'TrueHD'} input for WAV decoding." | |
| ) | |
| layout_info = CHANNELS[self.config.wav_channels_config] | |
| gst_out_ch_id = layout_info["id"] | |
| out_ch_names = layout_info["names"] | |
| procs = [] | |
| for i, ch_name in enumerate(out_ch_names): | |
| suffix = ( | |
| f".{ch_name}.wav" | |
| if self.config.wav_no_numbers_in_filename | |
| else f".{str(i + 1).zfill(2)}_{ch_name}.wav" | |
| ) | |
| out_fpath = ( | |
| output_base_path.with_suffix(suffix) | |
| if output_base_path | |
| else input_file.with_suffix(suffix) | |
| ) | |
| gst_cmd = gst_cmd_builder(input_file, out_fpath, i, gst_out_ch_id) | |
| if self.config.wav_single_thread_decode: | |
| print(f'Decoding "{out_fpath.name}" sequentially...') | |
| subprocess.run(gst_cmd, check=True, env=self.custom_env) | |
| else: | |
| print(f"Starting parallel decode for: {out_fpath.name}") | |
| procs.append(subprocess.Popen(gst_cmd, env=self.custom_env)) | |
| for i, p in enumerate(procs): | |
| p.wait() | |
| if p.returncode != 0: | |
| print( | |
| f"Warning: Decode process for channel {i+1} ({p.args}) failed (code {p.returncode}).", | |
| file=sys.stderr, | |
| ) | |
| print("Info: WAV decoding tasks submitted/completed.") | |
| elif self.config.mode == "thd_atmos_files": | |
| if not is_truehd: | |
| print( | |
| "Warning: Input not definitively TrueHD. GStreamer will attempt PCM+metadata extraction.", | |
| file=sys.stderr, | |
| ) | |
| out_fname_base = input_file.stem | |
| out_dir = pathlib.Path.cwd() | |
| if output_base_path: | |
| out_fname_base = output_base_path.stem | |
| out_dir = output_base_path.parent | |
| if not out_dir.exists(): | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| out_pcm_file = out_dir / f"{out_fname_base}.pcm" | |
| out_metadata_file = out_dir / f"{out_fname_base}.metadata" | |
| out_caf_file = out_dir / f"{out_fname_base}.caf" | |
| out_atmos_file = out_dir / f"{out_fname_base}.atmos" | |
| print(f'Info: Decoding "{input_file.name}" to PCM and Atmos Metadata...') | |
| gst_cmd = self._build_gstreamer_truehd_to_pcm_meta_cmd( | |
| input_file, out_pcm_file, out_metadata_file | |
| ) | |
| subprocess.run(gst_cmd, check=True, env=self.custom_env) | |
| print(f" PCM output: {out_pcm_file.name}") | |
| print(f" Metadata output: {out_metadata_file.name}") | |
| ffmpeg_exe = shutil.which(str(self.config.ffmpeg_exe_path)) | |
| if not ffmpeg_exe: | |
| raise RuntimeError( | |
| f"ffmpeg executable not found at '{self.config.ffmpeg_exe_path}' or in PATH." | |
| ) | |
| print( | |
| f"Info: Converting PCM '{out_pcm_file.name}' to CAF '{out_caf_file.name}' using ffmpeg..." | |
| ) | |
| ffmpeg_cmd = [ | |
| ffmpeg_exe, | |
| "-y", | |
| "-f", | |
| "s24le", | |
| "-ar", | |
| "48k", | |
| "-ac", | |
| "16", | |
| "-i", | |
| str(out_pcm_file), | |
| "-c:a", | |
| "copy", | |
| str(out_caf_file), | |
| ] | |
| subprocess.run(ffmpeg_cmd, check=True) | |
| print(f" CAF output: {out_caf_file.name}") | |
| print(f"Info: Generating .atmos file '{out_atmos_file.name}'...") | |
| atmos_content = self._generate_atmos_file_content( | |
| out_metadata_file.name, out_caf_file.name | |
| ) | |
| with open(out_atmos_file, "w", encoding="utf-8") as f: | |
| f.write(atmos_content) | |
| print(f" .atmos output: {out_atmos_file.name}") | |
| print( | |
| "Info: TrueHD to Atmos files generation complete. Outputs: .atmos, .metadata, .caf (and .pcm)." | |
| ) | |
| elif self.config.mode == "eac3_adm_bwf": | |
| if not is_eac3: | |
| print( | |
| "Warning: Input not definitively E-AC3. CavernizeGUI will attempt processing.", | |
| file=sys.stderr, | |
| ) | |
| out_adm_file = ( | |
| output_base_path.with_suffix(".wav") | |
| if output_base_path | |
| else input_file.with_name(f"{input_file.stem}_ADM_BWF.wav") | |
| ) | |
| cav_cmd = self._build_cavernize_eac3_to_adm_cmd(input_file, out_adm_file) | |
| print( | |
| f'Info: Decoding "{input_file.name}" to ADM BWF using CavernizeGUI...' | |
| ) | |
| print(f" Output ADM BWF: {out_adm_file.name}") | |
| print(f"Executing: {' '.join(map(str,cav_cmd))}") | |
| subprocess.run(cav_cmd, check=True, env=self.custom_env) | |
| print("Info: E-AC3 to ADM BWF decoding completed.") | |
| elif self.config.mode == "binaural": | |
| if not (is_eac3 or is_truehd): | |
| raise RuntimeError( | |
| "'binaural' mode requires identifiable E-AC3 or TrueHD input." | |
| ) | |
| print( | |
| f"Info: Detected {'E-AC3' if is_eac3 else 'TrueHD'} input for binaural rendering." | |
| ) | |
| out_fpath = ( | |
| output_base_path.with_suffix(".wav") | |
| if output_base_path | |
| else input_file.with_suffix(".binaural.wav") | |
| ) | |
| gst_cmd_builder = ( | |
| self._build_gstreamer_eac3_binaural_cmd | |
| if is_eac3 | |
| else self._build_gstreamer_truehd_binaural_cmd | |
| ) | |
| gst_cmd = gst_cmd_builder(input_file, out_fpath) | |
| print(f'Decoding "{out_fpath.name}"...') | |
| subprocess.run(gst_cmd, check=True, env=self.custom_env) | |
| print("Info: Binaural rendering completed.") | |
| else: | |
| raise RuntimeError(f"Unknown processing mode: {self.config.mode}") | |
| def main(): | |
| available_modes = ["wav", "eac3_adm_bwf", "thd_atmos_files", "binaural"] | |
| parser = argparse.ArgumentParser( | |
| description=f"{SCRIPT_NAME} v{SCRIPT_VERSION} - Decodes Dolby Atmos audio files on macOS.", | |
| formatter_class=argparse.RawTextHelpFormatter, | |
| ) | |
| parser.add_argument( | |
| "-i", | |
| "--input", | |
| help="Path to source audio file (E-AC3 or TrueHD).", | |
| type=pathlib.Path, | |
| required=True, | |
| ) | |
| parser.add_argument( | |
| "-o", | |
| "--output", | |
| type=pathlib.Path, | |
| help="Base path/name for output file(s).\n" | |
| '- "wav" mode: Suffixes like ".01_L.wav" are added. Default: beside input.\n' | |
| '- "thd_atmos_files" mode: Base for ".atmos", ".metadata", ".caf", ".pcm". Default: beside input.\n' | |
| '- "eac3_adm_bwf" mode: Full path for ".wav" ADM BWF. Default: "input_ADM_BWF.wav" beside input.\n' | |
| '- "binaural" mode: Full path for binaural ".wav" output. Default: "input.binaural.wav"', | |
| ) | |
| parser.add_argument( | |
| "--mode", | |
| default="wav", | |
| choices=available_modes, | |
| type=str, | |
| help="Processing mode:\n" | |
| " wav: (Default) Decode to individual channel WAV files (GStreamer).\n" | |
| " thd_atmos_files: Decode TrueHD to .atmos, .metadata, .caf, and .pcm files (GStreamer + ffmpeg).\n" | |
| " eac3_adm_bwf: Decode E-AC3 to ADM BWF (.wav) (CavernizeGUI + Wine).\n" | |
| " binaural: Binaural rendering", | |
| ) | |
| gst_group = parser.add_argument_group( | |
| 'GStreamer Options (for "wav", "thd_atmos_files", "binaural")' | |
| ) | |
| gst_group.add_argument( | |
| "--gst_exe_path", | |
| help="Path to 'gst-launch-1.0'. Default: '/Applications/Dolby/Dolby Reference Player.app/Contents/Frameworks/GStreamer.framework/Versions/1_16/Resources/bin/gst-launch-1.0'.", | |
| type=pathlib.Path, | |
| default=pathlib.Path( | |
| "/Applications/Dolby/Dolby Reference Player.app/Contents/Frameworks/GStreamer.framework/Versions/1_16/Resources/bin/gst-launch-1.0" | |
| ), | |
| ) | |
| wav_group = parser.add_argument_group('Options for "wav" mode') | |
| wav_group.add_argument( | |
| "-c", | |
| "--wav_channels_config", | |
| help='Output channel config (e.g., 7.1.4). Required for "wav" mode.', | |
| type=str, | |
| choices=CHANNELS.keys(), | |
| ) | |
| wav_group.add_argument( | |
| "--wav_no_numbers_in_filename", | |
| help='Omit numeric prefixes in WAV filenames (e.g., "L.wav").', | |
| action="store_true", | |
| ) | |
| wav_group.add_argument( | |
| "--wav_single_thread_decode", | |
| help="Decode WAV channels sequentially (slower, less CPU).", | |
| action="store_true", | |
| ) | |
| cav_group = parser.add_argument_group('Options for "eac3_adm_bwf" mode') | |
| cav_group.add_argument( | |
| "--cavernize_exe_path", | |
| help='Path to CavernizeGUI.exe. Required for "eac3_adm_bwf".', | |
| type=pathlib.Path, | |
| ) | |
| atmos_files_group = parser.add_argument_group('Options for "thd_atmos_files" mode') | |
| atmos_files_group.add_argument( | |
| "--ffmpeg_path", | |
| type=pathlib.Path, | |
| default=pathlib.Path("ffmpeg"), | |
| help="Path to 'ffmpeg' executable. Default: 'ffmpeg' (from PATH).", | |
| ) | |
| atmos_files_group.add_argument( | |
| "--atmos_fps", | |
| type=float, | |
| default=24.0, | |
| help="FPS for .atmos file. Default: 24.0.", | |
| ) | |
| atmos_files_group.add_argument( | |
| "--atmos_tool_name", | |
| type=str, | |
| default="YUYGFGG", | |
| help="Creation tool name for .atmos file. Default: YUYGFGG.", | |
| ) | |
| atmos_files_group.add_argument( | |
| "--atmos_tool_version", | |
| type=str, | |
| default=SCRIPT_VERSION, | |
| help=f"Creation tool version for .atmos file. Default: {SCRIPT_VERSION}.", | |
| ) | |
| args = parser.parse_args() | |
| if args.mode == "wav" and not args.wav_channels_config: | |
| parser.error("--wav_channels_config is required for 'wav' mode.") | |
| if args.mode == "eac3_adm_bwf" and not args.cavernize_exe_path: | |
| parser.error("--cavernize_exe_path is required for 'eac3_adm_bwf' mode.") | |
| script_config = Config( | |
| mode=args.mode, | |
| gst_exe_path=args.gst_exe_path, | |
| wav_channels_config=args.wav_channels_config, | |
| wav_no_numbers_in_filename=args.wav_no_numbers_in_filename, | |
| wav_single_thread_decode=args.wav_single_thread_decode, | |
| cavernize_exe_path=args.cavernize_exe_path, | |
| ffmpeg_exe_path=args.ffmpeg_path, | |
| atmos_fps=args.atmos_fps, | |
| atmos_tool_name=args.atmos_tool_name, | |
| atmos_tool_version=args.atmos_tool_version, | |
| ) | |
| try: | |
| decoder = AtmosDecode(script_config) | |
| decoder.decode(args.input, args.output) | |
| print(f"Info: Processing via mode '{args.mode}' completed successfully.") | |
| except RuntimeError as e: | |
| raise RuntimeError(e) | |
| except subprocess.CalledProcessError as e: | |
| cmd_str = " ".join(map(str, e.cmd)) if isinstance(e.cmd, list) else str(e.cmd) | |
| stdout = ( | |
| f"Stdout:\n{e.stdout.decode(errors='replace') if isinstance(e.stdout, bytes) else str(e.stdout)}" | |
| if e.stdout | |
| else "Stdout: (Empty)" | |
| ) | |
| stderr = ( | |
| f"Stderr:\n{e.stderr.decode(errors='replace') if isinstance(e.stderr, bytes) else str(e.stderr)}" | |
| if e.stderr | |
| else "Stderr: (Empty)" | |
| ) | |
| raise RuntimeError( | |
| f"External command execution failed: {cmd_str}\n" | |
| f"Return code: {e.returncode}\n" | |
| f"{stdout}\n" | |
| f"{stderr}" | |
| ) | |
| except Exception as e: | |
| raise RuntimeError(f"An unexpected error occurred: {e}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment