feat: merge save + preview (crossfade render, duration guard, ffplay preview)

This commit is contained in:
2026-07-02 16:18:29 +02:00
parent b8aa1606d8
commit 3922fc5174
2 changed files with 162 additions and 9 deletions
+67
View File
@@ -459,3 +459,70 @@ def test_merge_add_stores_duration_role(win, tmp_path):
win._merge_add_paths([str(a)])
# empty fixture -> ffprobe fails -> duration is None; M4 must tolerate None
assert win._merge_list.item(0).data(Qt.ItemDataRole.UserRole + 1) is None
def test_merge_save_builds_command(win, tmp_path, monkeypatch):
import main as m, pytest
from PyQt6.QtCore import Qt
a = tmp_path / "a.wav"; b = tmp_path / "b.wav"; a.write_bytes(b""); b.write_bytes(b"")
win._merge_list.clear()
win._merge_add_paths([str(a), str(b)])
win._merge_list.item(0).setData(Qt.ItemDataRole.UserRole + 1, 5.0) # long enough
win._merge_list.item(1).setData(Qt.ItemDataRole.UserRole + 1, 5.0)
win._spn_crossfade.setValue(0.75)
seen = {}
class _Stop(Exception): pass
def fake(clips, xf, out):
seen.update(clips=clips, xf=xf, out=out); raise _Stop
monkeypatch.setattr(m, "build_crossfade_merge_command", fake)
monkeypatch.setattr(m.QFileDialog, "getSaveFileName",
staticmethod(lambda *a, **k: (str(tmp_path / "m.wav"), "")))
with pytest.raises(_Stop):
win._on_merge_save()
assert seen["clips"] == [str(a), str(b)]
assert seen["xf"] == 0.75
assert seen["out"].endswith(".wav")
def test_merge_save_blocks_short_clip(win, tmp_path, monkeypatch):
import main as m
from PyQt6.QtCore import Qt
a = tmp_path / "a.wav"; b = tmp_path / "b.wav"; a.write_bytes(b""); b.write_bytes(b"")
win._merge_list.clear()
win._merge_add_paths([str(a), str(b)])
win._merge_list.item(0).setData(Qt.ItemDataRole.UserRole + 1, 0.1) # shorter than crossfade
win._merge_list.item(1).setData(Qt.ItemDataRole.UserRole + 1, 5.0)
win._spn_crossfade.setValue(0.5)
calls = {"n": 0}
monkeypatch.setattr(m, "build_crossfade_merge_command",
lambda *a, **k: calls.__setitem__("n", calls["n"] + 1) or [])
monkeypatch.setattr(m.QFileDialog, "getSaveFileName",
staticmethod(lambda *a, **k: (_ for _ in ()).throw(AssertionError("dialog opened"))))
win._on_merge_save() # guard must block before dialog/builder
assert calls["n"] == 0
def test_merge_save_empty_is_noop(win):
win._merge_list.clear()
win._on_merge_save() # no raise, no dialog
def test_merge_preview_blocks_short_clip(win, tmp_path, monkeypatch):
import main as m
from PyQt6.QtCore import Qt
a = tmp_path / "a.wav"; b = tmp_path / "b.wav"; a.write_bytes(b""); b.write_bytes(b"")
win._merge_list.clear()
win._merge_add_paths([str(a), str(b)])
win._merge_list.item(0).setData(Qt.ItemDataRole.UserRole + 1, 0.1)
win._merge_list.item(1).setData(Qt.ItemDataRole.UserRole + 1, 5.0)
win._spn_crossfade.setValue(0.5)
calls = {"n": 0}
monkeypatch.setattr(m, "build_crossfade_merge_command",
lambda *a, **k: calls.__setitem__("n", calls["n"] + 1) or [])
win._on_merge_preview()
assert calls["n"] == 0
def test_merge_preview_empty_is_noop(win):
win._merge_list.clear()
win._on_merge_preview()