diff --git a/MkvOpusEnc.py b/MkvOpusEnc.py index 771a1c9..a782a40 100644 --- a/MkvOpusEnc.py +++ b/MkvOpusEnc.py @@ -15,38 +15,24 @@ import json import shutil import subprocess import sys -import re # Import the regular expression module import tempfile from datetime import datetime from pathlib import Path class Tee: - """A file-like object that writes to multiple files and handles terminal-specific output.""" def __init__(self, *files): self.files = files - # Identify which file objects are interactive terminals - self.terminals = [f for f in files if hasattr(f, 'isatty') and f.isatty()] - def write(self, obj): - # For regular file writes (like logs), replace carriage returns with newlines - # to ensure each progress update is on a new line in the log file. - log_obj = obj.replace('\r', '\n').replace('\x1B[K', '') - for f in self.files: - if f in self.terminals: - f.write(obj) # Write to terminal as-is - else: - # For log files, write the modified object. - f.write(log_obj) + f.write(obj) f.flush() - def flush(self): for f in self.files: f.flush() def check_tools(): """Checks if all required command-line tools are in the system's PATH.""" - required_tools = ["ffmpeg", "ffprobe", "mkvmerge", "opusenc", "mediainfo"] + required_tools = ["ffmpeg", "ffprobe", "mkvmerge", "sox_ng", "opusenc", "mediainfo"] print("--- Prerequisite Check ---") all_found = True for tool in required_tools: @@ -64,7 +50,7 @@ def run_cmd(args, capture_output=False, check=True): def convert_audio_track(stream_index, channels, temp_dir, source_file, should_downmix, bitrate_info): """Extracts, normalizes, and encodes a single audio track to Opus.""" - temp_extracted = temp_dir / f"track_{stream_index}_extracted.flac" # This will be the input for loudnorm pass 1 + temp_extracted = temp_dir / f"track_{stream_index}_extracted.flac" temp_normalized = temp_dir / f"track_{stream_index}_normalized.flac" final_opus = temp_dir / f"track_{stream_index}_final.opus" @@ -92,63 +78,9 @@ def convert_audio_track(stream_index, channels, temp_dir, source_file, should_do ffmpeg_args.extend(["-c:a", "flac", str(temp_extracted)]) run_cmd(ffmpeg_args) - # Step 2: Normalize the track with ffmpeg (loudnorm 2-pass) - print(" - Normalizing Audio Track with ffmpeg (loudnorm 2-pass)...") - # First pass: Analyze the audio to get loudnorm stats - # The stats are printed to stderr, so we must use subprocess.run directly to capture it. - print(" - Pass 1: Analyzing...") - - cmd = [ - "ffmpeg", "-v", "error", "-stats", "-i", str(temp_extracted), - "-af", "loudnorm=I=-18:LRA=7:tp=-1:print_format=json", "-f", "null", "-" - ] - - # Use Popen to capture stderr in real-time and print it, while also buffering for JSON parsing. - process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8') - - # Read stderr and print progress updates. The Tee class now handles '\r' correctly. - stderr_output = "" - with process.stderr as pipe: - for line in iter(pipe.readline, ''): - stderr_output += line # Buffer the full output for parsing - # Print the line to stdout, clearing the line and using a carriage return. - # This will be handled by the Tee class. - sys.stdout.write(line.strip() + '\x1B[K\r') - # After the loop, read any remaining data (the JSON block) - stderr_output += pipe.read() - - process.wait() # Wait for the process to terminate - if process.returncode != 0: - raise subprocess.CalledProcessError(process.returncode, cmd, output=process.stdout.read(), stderr="".join(stderr_buffer)) - - # Find the start of the JSON block in stderr and parse it. - # This is more robust than slicing the last N lines. - # We find the start and end of the JSON block to avoid parsing extra data. - json_start_index = stderr_output.find('{') - if json_start_index == -1: - raise ValueError("Could not find start of JSON block in ffmpeg output for loudnorm analysis.") - - brace_level = 0 - json_end_index = -1 - for i, char in enumerate(stderr_output[json_start_index:]): - if char == '{': - brace_level += 1 - elif char == '}': - brace_level -= 1 - if brace_level == 0: - json_end_index = json_start_index + i + 1 - break - - stats = json.loads(stderr_output[json_start_index:json_end_index]) - - # Second pass: Apply the normalization using the stats from the first pass. A final newline is needed. - # A print() is needed to move to the next line after the progress bar. - print("\n - Pass 2: Applying normalization...") - run_cmd([ - "ffmpeg", "-v", "quiet", "-stats", "-y", "-i", str(temp_extracted), "-af", - f"loudnorm=I=-18:LRA=7:tp=-1:measured_i={stats['input_i']}:measured_lra={stats['input_lra']}:measured_tp={stats['input_tp']}:measured_thresh={stats['input_thresh']}:offset={stats['target_offset']}", - "-c:a", "flac", str(temp_normalized) - ]) + # Step 2: Normalize the track with SoX NG + print(" - Normalizing with SoX...") + run_cmd(["sox_ng", str(temp_extracted), str(temp_normalized), "-S", "--temp", str(temp_dir), "--guard", "gain", "-n"]) # Step 3: Encode to Opus with the correct bitrate bitrate = "192k" # Fallback