diff --git a/README.md b/README.md index 63d42a8..f3b48e3 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ All clips are exactly 8 seconds — the standard length for foley sound datasets - **Output format** — WAV / MP3 / FLAC / M4A / OGG / OPUS, remembered between sessions - **Non-destructive edits** — fade in/out, EBU R128 normalize, and gain, applied via ffmpeg on extract (and audition) - **Waveform preview** — a read-only strip of the current area (↻ to refresh; decode length-capped so long areas don't stall) +- **Interactive waveform** — drag the in/out handles to set the exact clip region, wheel to zoom, with a playhead during audition - **Audition** — Play/Stop the current area with the edits applied ### Audio merge (crossfade) diff --git a/main.py b/main.py index 140fc37..af9fb9a 100755 --- a/main.py +++ b/main.py @@ -4672,6 +4672,12 @@ class MainWindow(QMainWindow): "Audition the current audio area (with the edits applied)") self._btn_audio_play.toggled.connect(self._on_audio_audition) self._audition_proc = None # QProcess while auditioning, else None + from PyQt6.QtCore import QElapsedTimer + self._audition_start_t = 0.0 + self._audition_elapsed = QElapsedTimer() + self._audition_playhead_timer = QTimer(self) + self._audition_playhead_timer.setInterval(50) + self._audition_playhead_timer.timeout.connect(self._tick_audition_playhead) # ── Merge (crossfade) pane widgets ─────────────────────────── self._merge_list = QListWidget() @@ -5164,10 +5170,17 @@ class MainWindow(QMainWindow): # ── Changelog ──────────────────────────────────────────── - APP_VERSION = "1.4" + APP_VERSION = "1.5" _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.5", [ + "Interactive waveform — the Audio tab's waveform is now a " + "selection surface: drag the in/out handles (or drag a new " + "range) to set the exact clip, wheel to zoom, and a " + "playhead tracks audition. The selection stays in sync with " + "the length control both ways.", + ]), ("1.4", [ "Merge (crossfade) — a new Merge pane in the Audio tab " "assembles multiple clips into one, crossfading every join. Add the " @@ -6996,6 +7009,14 @@ class MainWindow(QMainWindow): return self._play_file(tmp) self._btn_audio_play.setText("■ Stop") + # start the playhead timer AFTER _play_file (it calls _stop_audition, which stops this timer) + self._audition_start_t = start # start = self._cursor used for the render + self._audition_elapsed.restart() + self._audition_playhead_timer.start() + + def _tick_audition_playhead(self) -> None: + secs = self._audition_elapsed.elapsed() / 1000.0 + self._wave.set_playhead(self._audition_start_t + secs) def _on_audition_error(self, _err) -> None: self._show_status("Audio playback unavailable (ffplay not found)", 4000) @@ -7010,6 +7031,8 @@ class MainWindow(QMainWindow): if proc is not None: proc.deleteLater() self._btn_audio_play.setText("▶ Play") + self._audition_playhead_timer.stop() + self._wave.set_playhead(None) # Snap the toggle back without re-entering start logic. if self._btn_audio_play.isChecked(): self._btn_audio_play.blockSignals(True) @@ -7029,6 +7052,8 @@ class MainWindow(QMainWindow): proc.waitForFinished(100) proc.deleteLater() self._btn_audio_play.setText("▶ Play") + self._audition_playhead_timer.stop() + self._wave.set_playhead(None) def _on_extract_audio(self) -> None: """Extract an exact-length audio slice starting at the playhead and diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index c71c0d3..65164e4 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -467,6 +467,13 @@ def test_audition_button_present_and_safe(win): assert win._audition_proc is None +def test_audition_clears_waveform_playhead(win): + win._wave.set_playhead(5.0) + win._stop_audition() + assert win._wave._playhead is None + assert not win._audition_playhead_timer.isActive() + + def test_merge_list_add_remove_reorder(win, tmp_path): from PyQt6.QtCore import Qt a = tmp_path / "a.wav"; b = tmp_path / "b.wav"