feat: extract honors format picker + fade/normalize/gain edits

This commit is contained in:
2026-07-02 14:47:49 +02:00
parent c9a4ac584f
commit 8e03b97742
2 changed files with 85 additions and 5 deletions
+21 -5
View File
@@ -37,7 +37,7 @@ from core.paths import _bin, _log, build_export_path, build_sequence_dir, format
from core.ffmpeg import (
_RATIOS, resolve_keyframe, apply_keyframes_to_jobs,
build_ffmpeg_command, build_audio_extract_command, build_audio_clip_command,
probe_duration, detect_hw_encoders,
audio_edit_filters, probe_duration, detect_hw_encoders,
)
from core.db import ProcessedDB
from core.annotations import remove_clip_annotation, upsert_clip_annotation
@@ -6471,23 +6471,39 @@ class MainWindow(QMainWindow):
return
start = self._cursor
dur = self._spn_audio_len.value()
ext = self._cmb_audio_fmt.currentData() or ".wav"
fmt_label = self._cmb_audio_fmt.currentText()
# No clamping: pass the requested length straight to ffmpeg. It stops
# cleanly at end-of-file if the source is shorter, and we report the
# actual length afterwards so any truncation is visible, not silent.
stem = os.path.splitext(os.path.basename(self._file_path))[0]
default_name = f"{stem}_{start:.2f}-{start + dur:.2f}s.wav"
default_name = f"{stem}_{start:.2f}-{start + dur:.2f}s{ext}"
default_dir = (self._settings.value("audio_extract_dir", "")
or self._tab_export_folder()
or os.path.dirname(self._file_path))
# Build the save filter from the combo so it stays in sync and the
# chosen format leads (stable sort keeps the rest in combo order).
_fmts = [(self._cmb_audio_fmt.itemText(i), self._cmb_audio_fmt.itemData(i))
for i in range(self._cmb_audio_fmt.count())]
_fmts.sort(key=lambda it: it[1] != ext)
save_filter = ";;".join([f"{lbl} (*{e})" for lbl, e in _fmts]
+ ["All files (*)"])
path, _sel = QFileDialog.getSaveFileName(
self, "Save audio clip", os.path.join(default_dir, default_name),
"WAV (*.wav);;MP3 (*.mp3);;FLAC (*.flac);;All files (*)")
save_filter)
if not path:
return
if not os.path.splitext(path)[1]:
path += ".wav"
path += ext
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
cmd = build_audio_clip_command(self._file_path, start, dur, path)
edit_filters = audio_edit_filters(
duration=dur,
fade_in=self._spn_fade_in.value(),
fade_out=self._spn_fade_out.value(),
normalize=self._chk_normalize.isChecked(),
gain_db=self._spn_gain.value())
cmd = build_audio_clip_command(self._file_path, start, dur, path,
filters=edit_filters or None)
self._btn_extract_audio.setEnabled(False)
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
self._show_status(f"Extracting {dur:.2f}s of audio…")
+64
View File
@@ -308,3 +308,67 @@ def test_audio_toolbox_has_two_panes(win):
assert win._btn_extract_audio in tb.findChildren(QPushButton)
# Scan controls remain reachable.
assert win._btn_scan in tb.findChildren(QPushButton)
def test_extract_honors_format_and_edits(win, monkeypatch, tmp_path):
import main as m
import pytest
win._file_path = "/x/video.mp4"
win._cursor = 5.0
win._spn_audio_len.setValue(4.0)
win._cmb_audio_fmt.setCurrentIndex(win._cmb_audio_fmt.findData(".mp3"))
win._spn_fade_in.setValue(0.5)
win._chk_normalize.setChecked(True)
seen = {}
class _Stop(Exception):
pass
def fake_cmd(inp, start, dur, out, filters=None):
seen["out"] = out
seen["filters"] = filters or []
raise _Stop
monkeypatch.setattr(m, "build_audio_clip_command", fake_cmd)
dialog = {}
def fake_savedialog(parent, title, default_path, filt):
dialog["default"] = default_path
dialog["filter"] = filt
return (str(tmp_path / "clip.mp3"), "")
monkeypatch.setattr(m.QFileDialog, "getSaveFileName",
staticmethod(fake_savedialog))
with pytest.raises(_Stop):
win._on_extract_audio()
# Default filename used the picked format's extension, and that format leads the filter.
assert dialog["default"].endswith(".mp3")
assert dialog["filter"].startswith("MP3")
# Output path + edit filters flowed into the command builder.
assert seen["out"].endswith(".mp3")
assert any("afade=t=in" in f for f in seen["filters"])
assert "loudnorm" in seen["filters"]
def test_extract_no_edits_passes_no_filters(win, monkeypatch, tmp_path):
import main as m
import pytest
win._file_path = "/x/video.mp4"
win._cursor = 5.0
win._spn_audio_len.setValue(4.0)
# all edit controls at default
win._spn_fade_in.setValue(0.0)
win._spn_fade_out.setValue(0.0)
win._chk_normalize.setChecked(False)
win._spn_gain.setValue(0.0)
seen = {}
class _Stop(Exception):
pass
def fake_cmd(inp, start, dur, out, filters=None):
seen["filters"] = filters
raise _Stop
monkeypatch.setattr(m, "build_audio_clip_command", fake_cmd)
monkeypatch.setattr(m.QFileDialog, "getSaveFileName",
staticmethod(lambda *a, **k: (str(tmp_path / "c.wav"), "")))
with pytest.raises(_Stop):
win._on_extract_audio()
assert seen["filters"] is None