feat: wire interactive waveform selection to cursor/length (two-way, clamped view)
This commit is contained in:
@@ -4111,6 +4111,7 @@ class MainWindow(QMainWindow):
|
||||
# State
|
||||
self._file_path: str = ""
|
||||
self._cursor: float = 0.0
|
||||
self._wave_syncing = False
|
||||
self._export_counter: int = 1
|
||||
self._export_worker: ExportWorker | None = None
|
||||
self._export_queue: list[dict] = []
|
||||
@@ -4658,6 +4659,13 @@ class MainWindow(QMainWindow):
|
||||
self._btn_wave_refresh.setToolTip(
|
||||
"Decode the current audio area and draw its waveform")
|
||||
self._btn_wave_refresh.clicked.connect(self._on_wave_refresh)
|
||||
self._wave.selection_changed.connect(self._on_wave_selection_changed)
|
||||
self._wave.view_changed.connect(self._on_wave_view_changed)
|
||||
self._wave_pending_view = None
|
||||
self._wave_decode_timer = QTimer(self)
|
||||
self._wave_decode_timer.setSingleShot(True)
|
||||
self._wave_decode_timer.setInterval(150)
|
||||
self._wave_decode_timer.timeout.connect(self._decode_pending_wave)
|
||||
self._btn_audio_play = QPushButton("▶ Play")
|
||||
self._btn_audio_play.setCheckable(True)
|
||||
self._btn_audio_play.setToolTip(
|
||||
@@ -5158,6 +5166,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
APP_VERSION = "1.4"
|
||||
_SPLIT_HEADER_H = 22 # deck split-column header height (keep both deck spots in sync)
|
||||
_WAVE_MIN_VIEW = 3.0 # minimum waveform view window (seconds)
|
||||
CHANGELOG: list[tuple[str, list[str]]] = [
|
||||
("1.4", [
|
||||
"<b>Merge (crossfade)</b> — a new <b>Merge</b> pane in the Audio tab "
|
||||
@@ -6704,6 +6713,8 @@ class MainWindow(QMainWindow):
|
||||
def _on_audio_len_changed(self, value: float) -> None:
|
||||
self._settings.setValue("audio_extract_len", value)
|
||||
self._update_audio_region()
|
||||
if not self._wave_syncing and self._wave.selection() is not None:
|
||||
self._wave.set_selection(self._cursor, self._cursor + value)
|
||||
|
||||
def _update_audio_region(self) -> None:
|
||||
"""Keep the timeline's audio-area band in sync with the playhead and
|
||||
@@ -6881,25 +6892,69 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.warning(self, "Preview failed",
|
||||
f"Could not render preview:\n\n{err}")
|
||||
|
||||
def _on_wave_refresh(self) -> None:
|
||||
"""Decode the current audio area and repaint the waveform strip."""
|
||||
def _decode_wave(self, view_start: float, view_dur: float) -> bool:
|
||||
"""Decode + paint the waveform for [view_start, view_start+view_dur] (capped).
|
||||
Returns True if audio was decoded, False otherwise."""
|
||||
if not self._file_path:
|
||||
self._wave.clear()
|
||||
return
|
||||
return False
|
||||
from core.waveform import load_region_samples, peaks
|
||||
start = self._cursor
|
||||
dur = self._spn_audio_len.value()
|
||||
preview = min(dur, 120.0) # cap decode; the strip is 128 bars regardless
|
||||
preview = min(view_dur, 120.0)
|
||||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||||
try:
|
||||
samples = load_region_samples(self._file_path, start, preview)
|
||||
samples = load_region_samples(self._file_path, view_start, preview)
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
self._wave.set_peaks(peaks(samples))
|
||||
if samples.size == 0:
|
||||
return samples.size > 0
|
||||
|
||||
def _on_wave_selection_changed(self, s: float, e: float) -> None:
|
||||
if self._wave_syncing:
|
||||
return
|
||||
self._wave_syncing = True
|
||||
try:
|
||||
self._cursor = s
|
||||
self._timeline.set_cursor(s)
|
||||
if self._file_path:
|
||||
try:
|
||||
self._mpv.seek(s)
|
||||
except Exception:
|
||||
pass
|
||||
self._spn_audio_len.setValue(e - s) # fires _on_audio_len_changed (guarded)
|
||||
self._update_audio_region()
|
||||
finally:
|
||||
self._wave_syncing = False
|
||||
|
||||
def _on_wave_view_changed(self, view_start: float, view_dur: float) -> None:
|
||||
src = self._timeline._duration
|
||||
if src and src > 0:
|
||||
view_dur = min(view_dur, src)
|
||||
view_start = max(0.0, min(view_start, max(0.0, src - view_dur)))
|
||||
self._wave.set_view(view_start, view_dur) # re-apply clamped window (cheap repaint)
|
||||
self._wave_pending_view = (view_start, view_dur)
|
||||
self._wave_decode_timer.start() # coalesce rapid wheel zoom
|
||||
|
||||
def _decode_pending_wave(self) -> None:
|
||||
if self._wave_pending_view is None:
|
||||
return
|
||||
vs, vd = self._wave_pending_view
|
||||
self._wave_pending_view = None
|
||||
self._decode_wave(vs, vd)
|
||||
|
||||
def _on_wave_refresh(self) -> None:
|
||||
if not self._file_path:
|
||||
self._wave.clear()
|
||||
return
|
||||
start = self._cursor
|
||||
length = self._spn_audio_len.value()
|
||||
view_dur = max(length, self._WAVE_MIN_VIEW)
|
||||
self._wave.set_view(start, view_dur)
|
||||
ok = self._decode_wave(start, view_dur)
|
||||
self._wave.set_selection(start, start + length)
|
||||
if not ok:
|
||||
self._show_status("Waveform: no audio decoded", 3000)
|
||||
elif preview < dur:
|
||||
self._show_status(f"Waveform shows first {preview:.0f}s of {dur:.0f}s", 4000)
|
||||
elif view_dur > 120.0:
|
||||
self._show_status(f"Waveform shows first 120s of the {view_dur:.0f}s view", 4000)
|
||||
|
||||
def _current_edit_filters(self) -> list[str]:
|
||||
"""ffmpeg -af chain for the current edit controls (shared by extract +
|
||||
|
||||
Reference in New Issue
Block a user