Revertes MkvOpusEnc to use Sox_NG again
This commit is contained in:
@@ -15,38 +15,24 @@ import json
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import re # Import the regular expression module
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
class Tee:
|
class Tee:
|
||||||
"""A file-like object that writes to multiple files and handles terminal-specific output."""
|
|
||||||
def __init__(self, *files):
|
def __init__(self, *files):
|
||||||
self.files = 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):
|
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:
|
for f in self.files:
|
||||||
if f in self.terminals:
|
f.write(obj)
|
||||||
f.write(obj) # Write to terminal as-is
|
|
||||||
else:
|
|
||||||
# For log files, write the modified object.
|
|
||||||
f.write(log_obj)
|
|
||||||
f.flush()
|
f.flush()
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
for f in self.files:
|
for f in self.files:
|
||||||
f.flush()
|
f.flush()
|
||||||
|
|
||||||
def check_tools():
|
def check_tools():
|
||||||
"""Checks if all required command-line tools are in the system's PATH."""
|
"""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 ---")
|
print("--- Prerequisite Check ---")
|
||||||
all_found = True
|
all_found = True
|
||||||
for tool in required_tools:
|
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):
|
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."""
|
"""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"
|
temp_normalized = temp_dir / f"track_{stream_index}_normalized.flac"
|
||||||
final_opus = temp_dir / f"track_{stream_index}_final.opus"
|
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)])
|
ffmpeg_args.extend(["-c:a", "flac", str(temp_extracted)])
|
||||||
run_cmd(ffmpeg_args)
|
run_cmd(ffmpeg_args)
|
||||||
|
|
||||||
# Step 2: Normalize the track with ffmpeg (loudnorm 2-pass)
|
# Step 2: Normalize the track with SoX NG
|
||||||
print(" - Normalizing Audio Track with ffmpeg (loudnorm 2-pass)...")
|
print(" - Normalizing with SoX...")
|
||||||
# First pass: Analyze the audio to get loudnorm stats
|
run_cmd(["sox_ng", str(temp_extracted), str(temp_normalized), "-S", "--temp", str(temp_dir), "--guard", "gain", "-n"])
|
||||||
# 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 3: Encode to Opus with the correct bitrate
|
# Step 3: Encode to Opus with the correct bitrate
|
||||||
bitrate = "192k" # Fallback
|
bitrate = "192k" # Fallback
|
||||||
|
|||||||
Reference in New Issue
Block a user