feat: global crossfade curve combo + per-join render/guard plumbing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 17:40:01 +02:00
co-authored by Claude Opus 4.8
parent a298418156
commit 65200e482e
2 changed files with 75 additions and 23 deletions
+42 -20
View File
@@ -4694,6 +4694,12 @@ class MainWindow(QMainWindow):
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._cmb_curve = QComboBox()
for label, name in (("Triangular", "tri"), ("Exponential", "exp"),
("Logarithmic", "log"), ("Quarter sine", "qsin"),
("Half sine", "hsin")):
self._cmb_curve.addItem(label, name)
self._cmb_curve.setToolTip("Crossfade curve at each join")
self._btn_merge_add_sel = QPushButton(" Selection")
self._btn_merge_add_sel.setToolTip(
"Add the current audio area (with edits) as a clip")
@@ -5050,6 +5056,8 @@ class MainWindow(QMainWindow):
xrow = QHBoxLayout()
xrow.addWidget(QLabel("Crossfade:"))
xrow.addWidget(self._spn_crossfade)
xrow.addWidget(QLabel("Curve:"))
xrow.addWidget(self._cmb_curve)
xrow.addStretch()
mv.addLayout(xrow)
brow = QHBoxLayout()
@@ -6749,6 +6757,8 @@ class MainWindow(QMainWindow):
item = QListWidgetItem(text)
item.setData(Qt.ItemDataRole.UserRole, ap)
item.setData(Qt.ItemDataRole.UserRole + 1, dur)
item.setData(Qt.ItemDataRole.UserRole + 2, None) # crossfade override (None = global)
item.setData(Qt.ItemDataRole.UserRole + 3, None) # curve override (None = global)
self._merge_list.addItem(item)
finally:
QApplication.restoreOverrideCursor()
@@ -6811,22 +6821,33 @@ class MainWindow(QMainWindow):
return [self._merge_list.item(i).data(Qt.ItemDataRole.UserRole)
for i in range(self._merge_list.count())]
def _merge_guard_ok(self, crossfade: float) -> bool:
"""False (+ status) if a clip with a KNOWN duration is <= the crossfade
(acrossfade needs each clip longer than the crossfade)."""
if crossfade <= 0:
return True
# Clips whose duration couldn't be probed (None) are not flagged here;
# if genuinely too short they surface as an ffmpeg error at render.
for i in range(self._merge_list.count()):
it = self._merge_list.item(i)
d = it.data(Qt.ItemDataRole.UserRole + 1)
if d is not None and d <= crossfade:
name = os.path.basename(it.data(Qt.ItemDataRole.UserRole))
self._show_status(
f"'{name}' ({d:.2f}s) is shorter than the "
f"{crossfade:.2f}s crossfade", 6000)
return False
def _merge_join_values(self) -> tuple[list[float], list[str]]:
n = self._merge_list.count()
xfs, cvs = [], []
for j in range(n - 1):
it = self._merge_list.item(j)
xo = it.data(Qt.ItemDataRole.UserRole + 2)
co = it.data(Qt.ItemDataRole.UserRole + 3)
xfs.append(float(xo) if xo is not None else self._spn_crossfade.value())
cvs.append(co if co is not None else self._cmb_curve.currentData())
return xfs, cvs
def _merge_guard_ok(self) -> bool:
"""False (+ status) if a clip with a KNOWN duration is <= its join's
crossfade (acrossfade needs both sides longer than the crossfade)."""
xfs, _cvs = self._merge_join_values()
for j, xf in enumerate(xfs):
if xf <= 0:
continue
for idx in (j, j + 1):
it = self._merge_list.item(idx)
d = it.data(Qt.ItemDataRole.UserRole + 1)
if d is not None and d <= xf:
name = os.path.basename(it.data(Qt.ItemDataRole.UserRole))
self._show_status(
f"'{name}' ({d:.2f}s) is shorter than its "
f"{xf:.2f}s crossfade", 6000)
return False
return True
def _play_file(self, path: str) -> None:
@@ -6841,8 +6862,9 @@ class MainWindow(QMainWindow):
def _render_merge(self, out_path: str) -> tuple[bool, str]:
"""Render the merge sequence to out_path. Returns (ok, last_stderr_line)."""
cmd = build_crossfade_merge_command(
self._merge_paths(), self._spn_crossfade.value(), out_path)
xfs, cvs = self._merge_join_values()
cmd = build_crossfade_merge_command(self._merge_paths(), xfs, out_path,
curves=cvs)
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
err = ""
try:
@@ -6862,7 +6884,7 @@ class MainWindow(QMainWindow):
if not paths:
self._show_status("Add clips to merge first", 3000)
return
if not self._merge_guard_ok(self._spn_crossfade.value()):
if not self._merge_guard_ok():
return
default_dir = (self._settings.value("audio_extract_dir", "")
or os.path.dirname(self._file_path or ""))
@@ -6890,7 +6912,7 @@ class MainWindow(QMainWindow):
if not paths:
self._show_status("Add clips to merge first", 3000)
return
if not self._merge_guard_ok(self._spn_crossfade.value()):
if not self._merge_guard_ok():
return
import tempfile
fd, tmp = tempfile.mkstemp(suffix=".wav", prefix="8cut_merge_prev_")