feat: Merge pane (3rd Audio QToolBox page) — list + crossfade + buttons

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:54:05 +02:00
co-authored by Claude Opus 4.8
parent 52417ea1ee
commit de7da3cad6
2 changed files with 86 additions and 0 deletions
+76
View File
@@ -4538,6 +4538,43 @@ class MainWindow(QMainWindow):
"Audition the current audio area (with the edits applied)") "Audition the current audio area (with the edits applied)")
self._btn_audio_play.toggled.connect(self._on_audio_audition) self._btn_audio_play.toggled.connect(self._on_audio_audition)
self._audition_proc = None # QProcess while auditioning, else None self._audition_proc = None # QProcess while auditioning, else None
# ── Merge (crossfade) pane widgets ───────────────────────────
self._merge_list = QListWidget()
self._merge_list.setSelectionMode(
QAbstractItemView.SelectionMode.SingleSelection)
self._merge_list.setToolTip("Clips to merge, top to bottom")
self._spn_crossfade = QDoubleSpinBox()
self._spn_crossfade.setRange(0.0, 10.0)
self._spn_crossfade.setDecimals(2)
self._spn_crossfade.setSingleStep(0.1)
self._spn_crossfade.setSuffix(" s")
self._spn_crossfade.setToolTip("Crossfade applied at every join (0 = butt-join)")
self._spn_crossfade.setValue(float(self._settings.value("audio_crossfade", 0.5)))
self._spn_crossfade.valueChanged.connect(
lambda v: self._settings.setValue("audio_crossfade", v))
self._btn_merge_add_sel = QPushButton(" Selection")
self._btn_merge_add_sel.setToolTip(
"Add the current audio area (with edits) as a clip")
self._btn_merge_add_sel.clicked.connect(self._on_merge_add_selection)
self._btn_merge_add_file = QPushButton(" File…")
self._btn_merge_add_file.setToolTip("Add audio file(s) as clips")
self._btn_merge_add_file.clicked.connect(self._on_merge_add_file)
self._btn_merge_up = QPushButton("")
self._btn_merge_up.setToolTip("Move selected clip up")
self._btn_merge_up.clicked.connect(lambda: self._merge_move(-1))
self._btn_merge_down = QPushButton("")
self._btn_merge_down.setToolTip("Move selected clip down")
self._btn_merge_down.clicked.connect(lambda: self._merge_move(1))
self._btn_merge_remove = QPushButton("")
self._btn_merge_remove.setToolTip("Remove selected clip")
self._btn_merge_remove.clicked.connect(self._merge_remove_selected)
self._btn_merge_preview = QPushButton("▶ Preview")
self._btn_merge_preview.setToolTip("Merge to a temp file and audition")
self._btn_merge_preview.clicked.connect(self._on_merge_preview)
self._btn_merge_save = QPushButton("Merge && Save…")
self._btn_merge_save.setToolTip("Merge the clips and save the result")
self._btn_merge_save.clicked.connect(self._on_merge_save)
self._transport_row = transport_row self._transport_row = transport_row
# Row 1b — subcategory (subprofile) export buttons live on their own # Row 1b — subcategory (subprofile) export buttons live on their own
@@ -4864,6 +4901,27 @@ class MainWindow(QMainWindow):
sg.setColumnStretch(3, 1) sg.setColumnStretch(3, 1)
tb.addItem(scan_page, "Scan / Classify") tb.addItem(scan_page, "Scan / Classify")
# ── Page 3: Merge (crossfade) ────────────────────────────
merge_page = QWidget()
mv = QVBoxLayout(merge_page)
mv.setContentsMargins(8, 6, 8, 6); mv.setSpacing(6)
mv.addWidget(self._merge_list)
xrow = QHBoxLayout()
xrow.addWidget(QLabel("Crossfade:"))
xrow.addWidget(self._spn_crossfade)
xrow.addStretch()
mv.addLayout(xrow)
brow = QHBoxLayout()
for b in (self._btn_merge_add_sel, self._btn_merge_add_file,
self._btn_merge_up, self._btn_merge_down, self._btn_merge_remove):
brow.addWidget(b)
mv.addLayout(brow)
arow = QHBoxLayout()
arow.addWidget(self._btn_merge_preview)
arow.addWidget(self._btn_merge_save)
mv.addLayout(arow)
tb.addItem(merge_page, "Merge")
outer.addWidget(tb) outer.addWidget(tb)
# ── Menu bar ───────────────────────────────────────────── # ── Menu bar ─────────────────────────────────────────────
@@ -6521,6 +6579,24 @@ class MainWindow(QMainWindow):
start = self._cursor start = self._cursor
self._timeline.set_audio_region(start, start + self._spn_audio_len.value()) self._timeline.set_audio_region(start, start + self._spn_audio_len.value())
def _on_merge_add_selection(self) -> None:
pass # implemented in M3
def _on_merge_add_file(self) -> None:
pass # implemented in M3
def _merge_move(self, delta: int) -> None:
pass # implemented in M3
def _merge_remove_selected(self) -> None:
pass # implemented in M3
def _on_merge_preview(self) -> None:
pass # implemented in M4
def _on_merge_save(self) -> None:
pass # implemented in M4
def _on_wave_refresh(self) -> None: def _on_wave_refresh(self) -> None:
"""Decode the current audio area and repaint the waveform strip.""" """Decode the current audio area and repaint the waveform strip."""
if not self._file_path: if not self._file_path:
+10
View File
@@ -310,6 +310,16 @@ def test_audio_toolbox_has_two_panes(win):
assert win._btn_scan in tb.findChildren(QPushButton) assert win._btn_scan in tb.findChildren(QPushButton)
def test_merge_pane_present(win):
from PyQt6.QtWidgets import QToolBox, QListWidget, QDoubleSpinBox
tb = win._tab_audio.findChild(QToolBox)
titles = [tb.itemText(i) for i in range(tb.count())]
assert "Merge" in titles
assert isinstance(win._merge_list, QListWidget)
assert isinstance(win._spn_crossfade, QDoubleSpinBox)
assert win._spn_crossfade.value() == 0.5
def test_extract_honors_format_and_edits(win, monkeypatch, tmp_path): def test_extract_honors_format_and_edits(win, monkeypatch, tmp_path):
import main as m import main as m
import pytest import pytest