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:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user