added capture support for audio files.

This commit is contained in:
2026-02-12 15:02:47 +01:00
parent 5d7599891c
commit 47b452fe5f

90
main.py
View File

@@ -1,4 +1,5 @@
import os import os
import json
import threading import threading
import shutil import shutil
import subprocess import subprocess
@@ -34,6 +35,8 @@ class ConverterFrame(wx.Frame):
self.activation_bytes = wx.TextCtrl(panel, value="", size=(120, -1)) self.activation_bytes = wx.TextCtrl(panel, value="", size=(120, -1))
self.copy_streams = wx.CheckBox(panel, label="Co&py streams (fast, no re-encode)") self.copy_streams = wx.CheckBox(panel, label="Co&py streams (fast, no re-encode)")
self.copy_streams.SetForegroundColour(label_color) self.copy_streams.SetForegroundColour(label_color)
self.split_mp3_choice = wx.Choice(panel, choices=["No", "Yes"])
self.split_mp3_choice.SetSelection(0)
self.start_btn = wx.Button(panel, label="&Convert") self.start_btn = wx.Button(panel, label="&Convert")
self.log_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY) self.log_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY)
self.status = wx.StaticText(panel, label="Ready") self.status = wx.StaticText(panel, label="Ready")
@@ -63,6 +66,7 @@ class ConverterFrame(wx.Frame):
self.format_choice.SetName("Output format") self.format_choice.SetName("Output format")
self.configure_file_picker_accessibility(self.output_picker, "Output file") self.configure_file_picker_accessibility(self.output_picker, "Output file")
self.copy_streams.SetName("Copy streams") self.copy_streams.SetName("Copy streams")
self.split_mp3_choice.SetName("Split MP3 into captures")
self.start_btn.SetName("Convert") self.start_btn.SetName("Convert")
self.play_btn.SetName("Play or pause") self.play_btn.SetName("Play or pause")
self.stop_btn.SetName("Stop playback") self.stop_btn.SetName("Stop playback")
@@ -74,6 +78,7 @@ class ConverterFrame(wx.Frame):
self.video_bitrate.SetHelpText("Video bitrate") self.video_bitrate.SetHelpText("Video bitrate")
self.activation_bytes.SetHelpText("Activation bytes") self.activation_bytes.SetHelpText("Activation bytes")
self.copy_streams.SetHelpText("Copy streams") self.copy_streams.SetHelpText("Copy streams")
self.split_mp3_choice.SetHelpText("Split MP3 output into captures based on chapters")
self.start_btn.SetHelpText("Convert") self.start_btn.SetHelpText("Convert")
self.play_btn.SetHelpText("Play or pause") self.play_btn.SetHelpText("Play or pause")
self.stop_btn.SetHelpText("Stop playback") self.stop_btn.SetHelpText("Stop playback")
@@ -90,6 +95,7 @@ class ConverterFrame(wx.Frame):
self.add_labeled_control(panel, form, "Video b&itrate", self.video_bitrate, "Video bitrate") self.add_labeled_control(panel, form, "Video b&itrate", self.video_bitrate, "Video bitrate")
self.add_labeled_control(panel, form, "A&ctivation bytes", self.activation_bytes, "Activation bytes") self.add_labeled_control(panel, form, "A&ctivation bytes", self.activation_bytes, "Activation bytes")
form.Add(self.copy_streams, 0, wx.EXPAND | wx.BOTTOM, 6) form.Add(self.copy_streams, 0, wx.EXPAND | wx.BOTTOM, 6)
self.add_labeled_control(panel, form, "Split MP3 into &captures", self.split_mp3_choice, "Split MP3 into captures")
main = wx.BoxSizer(wx.VERTICAL) main = wx.BoxSizer(wx.VERTICAL)
main.Add(form, 0, wx.EXPAND | wx.ALL, 10) main.Add(form, 0, wx.EXPAND | wx.ALL, 10)
@@ -138,6 +144,7 @@ class ConverterFrame(wx.Frame):
choices = VIDEO_FORMATS choices = VIDEO_FORMATS
self.format_choice.Set(choices) self.format_choice.Set(choices)
self.format_choice.SetSelection(0) self.format_choice.SetSelection(0)
self.update_split_option_state()
def suggest_output_path(self): def suggest_output_path(self):
in_path = self.input_picker.GetPath() in_path = self.input_picker.GetPath()
@@ -147,6 +154,13 @@ class ConverterFrame(wx.Frame):
base, _ = os.path.splitext(in_path) base, _ = os.path.splitext(in_path)
suggested = base + "." + out_ext suggested = base + "." + out_ext
self.output_picker.SetPath(suggested) self.output_picker.SetPath(suggested)
self.update_split_option_state()
def update_split_option_state(self):
is_mp3 = self.format_choice.GetStringSelection().lower() == "mp3"
self.split_mp3_choice.Enable(is_mp3)
if not is_mp3:
self.split_mp3_choice.SetSelection(0)
def build_player(self, panel): def build_player(self, panel):
player = wx.BoxSizer(wx.VERTICAL) player = wx.BoxSizer(wx.VERTICAL)
@@ -352,6 +366,9 @@ class ConverterFrame(wx.Frame):
v_bitrate = self.video_bitrate.GetValue().strip() v_bitrate = self.video_bitrate.GetValue().strip()
activation_bytes = self.activation_bytes.GetValue().strip() activation_bytes = self.activation_bytes.GetValue().strip()
copy = self.copy_streams.GetValue() copy = self.copy_streams.GetValue()
split_mp3 = self.split_mp3_choice.GetStringSelection().lower() == "yes"
output_format = self.format_choice.GetStringSelection().lower()
should_split_mp3 = split_mp3 and output_format == "mp3"
cmd = ["ffmpeg", "-y"] cmd = ["ffmpeg", "-y"]
if activation_bytes: if activation_bytes:
@@ -373,7 +390,26 @@ class ConverterFrame(wx.Frame):
if a_bitrate: if a_bitrate:
cmd += ["-b:a", a_bitrate] cmd += ["-b:a", a_bitrate]
cmd.append(out_path) if should_split_mp3:
chapter_times = self.get_chapter_end_times(in_path)
if chapter_times:
output_pattern = self.build_capture_output_pattern(out_path)
cmd += [
"-f",
"segment",
"-segment_times",
",".join(chapter_times),
"-reset_timestamps",
"1",
"-segment_format",
"mp3",
output_pattern,
]
else:
self.append_log("No chapter markers found; writing a single MP3 file.")
cmd.append(out_path)
else:
cmd.append(out_path)
self.append_log("Command: " + " ".join(cmd)) self.append_log("Command: " + " ".join(cmd))
@@ -404,6 +440,58 @@ class ConverterFrame(wx.Frame):
wx.CallAfter(self.status.SetLabel, status_text) wx.CallAfter(self.status.SetLabel, status_text)
wx.CallAfter(self.start_btn.Enable) wx.CallAfter(self.start_btn.Enable)
def build_capture_output_pattern(self, out_path):
base, ext = os.path.splitext(out_path)
return f"{base}_capture_%03d{ext}"
def get_chapter_end_times(self, in_path):
if not shutil.which("ffprobe"):
self.append_log("ffprobe not found on PATH; cannot split by chapters.")
return []
cmd = [
"ffprobe",
"-v",
"error",
"-print_format",
"json",
"-show_chapters",
"-i",
in_path,
]
try:
proc = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
if proc.returncode != 0:
self.append_log("ffprobe failed; cannot split by chapters.")
return []
data = json.loads(proc.stdout or "{}")
except Exception as exc:
self.append_log(f"ffprobe error: {exc}")
return []
chapters = data.get("chapters", [])
end_times = []
for chapter in chapters:
end_time = chapter.get("end_time")
if end_time is None:
continue
try:
end_times.append(float(end_time))
except (TypeError, ValueError):
continue
if len(end_times) <= 1:
return []
end_times = end_times[:-1]
formatted = []
for value in end_times:
formatted.append(f"{value:.3f}".rstrip("0").rstrip("."))
return formatted
class App(wx.App): class App(wx.App):
def OnInit(self): def OnInit(self):