fix: separate editor playhead from selection
This commit is contained in:
@@ -4084,9 +4084,11 @@ class AudioWaveform(QWidget):
|
||||
decode)."""
|
||||
|
||||
selection_changed = pyqtSignal(float, float)
|
||||
playhead_changed = pyqtSignal(float)
|
||||
view_changed = pyqtSignal(float, float)
|
||||
|
||||
HANDLE_PX = 6
|
||||
CLICK_DRAG_PX = 3
|
||||
MIN_SPAN = 0.05
|
||||
MIN_SEL = 0.01
|
||||
|
||||
@@ -4097,7 +4099,9 @@ class AudioWaveform(QWidget):
|
||||
self._view_dur = 0.0
|
||||
self._sel: tuple[float, float] | None = None
|
||||
self._playhead: float | None = None
|
||||
self._markers: list[float] = []
|
||||
self._drag_anchor: float | None = None
|
||||
self._press_x: float | None = None
|
||||
self.setFixedHeight(96)
|
||||
self.setToolTip("Waveform of the current audio area (↻ to refresh)")
|
||||
|
||||
@@ -4124,14 +4128,32 @@ class AudioWaveform(QWidget):
|
||||
def selection(self) -> tuple[float, float] | None:
|
||||
return self._sel
|
||||
|
||||
def clear_selection(self) -> None:
|
||||
self._sel = None
|
||||
self.update()
|
||||
|
||||
def set_playhead(self, t: float | None) -> None:
|
||||
if t is not None and self._view_dur > 0:
|
||||
lo = self._view_start
|
||||
hi = self._view_start + self._view_dur
|
||||
t = min(max(t, lo), hi)
|
||||
self._playhead = t
|
||||
self.update()
|
||||
|
||||
def playhead(self) -> float | None:
|
||||
return self._playhead
|
||||
|
||||
def set_markers(self, markers: list[float] | None) -> None:
|
||||
self._markers = list(markers or [])
|
||||
self.update()
|
||||
|
||||
def clear(self) -> None:
|
||||
self._peaks = []
|
||||
self._sel = None
|
||||
self._playhead = None
|
||||
self._markers = []
|
||||
self._drag_anchor = None
|
||||
self._press_x = None
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, _ev):
|
||||
@@ -4153,6 +4175,12 @@ class AudioWaveform(QWidget):
|
||||
p.setPen(QPen(QColor(0, 220, 190), 2))
|
||||
p.drawLine(x1, 0, x1, h)
|
||||
p.drawLine(x2, 0, x2, h)
|
||||
if self._markers and self._view_dur > 0:
|
||||
p.setPen(QPen(QColor(255, 120, 0), 2))
|
||||
for marker in self._markers:
|
||||
if self._view_start <= marker <= self._view_start + self._view_dur:
|
||||
mx = t_to_x(marker, w, self._view_start, self._view_dur)
|
||||
p.drawLine(mx, 0, mx, h)
|
||||
if (self._playhead is not None and self._view_dur > 0
|
||||
and self._view_start <= self._playhead <= self._view_start + self._view_dur):
|
||||
px = t_to_x(self._playhead, w, self._view_start, self._view_dur)
|
||||
@@ -4169,16 +4197,22 @@ class AudioWaveform(QWidget):
|
||||
return t_to_x(t, self.width(), self._view_start, self._view_dur)
|
||||
|
||||
# --- drag-select / zoom -------------------------------------------------
|
||||
def _selection_handle_anchor(self, x: float) -> float | None:
|
||||
if self._sel is None or self._view_dur <= 0:
|
||||
return None
|
||||
xs = self._t_to_px(self._sel[0])
|
||||
xe = self._t_to_px(self._sel[1])
|
||||
if abs(x - xs) <= self.HANDLE_PX:
|
||||
return self._sel[1] # drag start; pivot on end
|
||||
if abs(x - xe) <= self.HANDLE_PX:
|
||||
return self._sel[0] # drag end; pivot on start
|
||||
return None
|
||||
|
||||
def _begin_drag_at_x(self, x: float) -> None:
|
||||
t = self._px_to_t(x)
|
||||
anchor = t
|
||||
if self._sel is not None and self._view_dur > 0:
|
||||
xs = self._t_to_px(self._sel[0])
|
||||
xe = self._t_to_px(self._sel[1])
|
||||
if abs(x - xs) <= self.HANDLE_PX:
|
||||
anchor = self._sel[1] # drag start; pivot on end
|
||||
elif abs(x - xe) <= self.HANDLE_PX:
|
||||
anchor = self._sel[0] # drag end; pivot on start
|
||||
anchor = self._selection_handle_anchor(x)
|
||||
if anchor is None:
|
||||
anchor = t
|
||||
self._drag_anchor = anchor
|
||||
self._drag_to_x(x)
|
||||
|
||||
@@ -4205,6 +4239,12 @@ class AudioWaveform(QWidget):
|
||||
if self._sel is not None:
|
||||
self.selection_changed.emit(self._sel[0], self._sel[1])
|
||||
|
||||
def _set_playhead_from_x(self, x: float, emit: bool = True) -> None:
|
||||
t = self._px_to_t(x)
|
||||
self.set_playhead(t)
|
||||
if emit and self._playhead is not None:
|
||||
self.playhead_changed.emit(self._playhead)
|
||||
|
||||
def _zoom_at_x(self, x: float, factor: float) -> None:
|
||||
if self._view_dur <= 0 or self.width() <= 0:
|
||||
return
|
||||
@@ -4219,14 +4259,28 @@ class AudioWaveform(QWidget):
|
||||
# --- Qt events ----------------------------------------------------------
|
||||
def mousePressEvent(self, e):
|
||||
if e.button() == Qt.MouseButton.LeftButton:
|
||||
self._begin_drag_at_x(e.position().x())
|
||||
x = e.position().x()
|
||||
self._press_x = x
|
||||
if self._selection_handle_anchor(x) is not None:
|
||||
self._begin_drag_at_x(x)
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
self._drag_to_x(e.position().x())
|
||||
x = e.position().x()
|
||||
if self._drag_anchor is not None:
|
||||
self._drag_to_x(x)
|
||||
elif (e.buttons() & Qt.MouseButton.LeftButton
|
||||
and self._press_x is not None
|
||||
and abs(x - self._press_x) >= self.CLICK_DRAG_PX):
|
||||
self._begin_drag_at_x(self._press_x)
|
||||
self._drag_to_x(x)
|
||||
|
||||
def mouseReleaseEvent(self, e):
|
||||
if e.button() == Qt.MouseButton.LeftButton:
|
||||
self._end_drag()
|
||||
if self._drag_anchor is not None:
|
||||
self._end_drag()
|
||||
elif self._press_x is not None:
|
||||
self._set_playhead_from_x(e.position().x())
|
||||
self._press_x = None
|
||||
|
||||
def wheelEvent(self, e):
|
||||
dy = e.angleDelta().y()
|
||||
@@ -4256,14 +4310,17 @@ class AudioEditorDialog(QDialog):
|
||||
self._play_speed = 1.0
|
||||
self._play_start = 0.0
|
||||
self._play_end: float | None = None
|
||||
self._play_loop = False
|
||||
self._play_elapsed = QElapsedTimer()
|
||||
self._playhead_timer = QTimer(self)
|
||||
self._playhead_timer.setInterval(33)
|
||||
self._playhead_timer.timeout.connect(self._tick_playhead)
|
||||
self._clip_dur = 0.0 # true duration of the current version
|
||||
self._join_preview: tuple[float, float] | None = None
|
||||
self._healed_seams: dict[str, list[float]] = {}
|
||||
|
||||
self._wave = AudioWaveform()
|
||||
self._wave.playhead_changed.connect(self._on_wave_playhead_changed)
|
||||
self._btn_heal_cut = QPushButton("Heal Cut")
|
||||
self._btn_delete = QPushButton("Delete")
|
||||
self._btn_silence = QPushButton("Silence")
|
||||
@@ -4347,10 +4404,15 @@ class AudioEditorDialog(QDialog):
|
||||
samples = load_region_samples(path, 0.0, min(dur, self._EDIT_DECODE_CAP))
|
||||
self._wave.set_view(0.0, dur)
|
||||
self._wave.set_peaks(peaks(samples, buckets=self._EDITOR_WAVEFORM_BINS))
|
||||
self._wave.set_selection(0.0, dur)
|
||||
self._wave.clear_selection()
|
||||
playhead = self._wave.playhead()
|
||||
self._wave.set_playhead(0.0 if playhead is None else min(playhead, dur))
|
||||
else:
|
||||
self._wave.set_view(0.0, 0.0)
|
||||
self._wave.set_peaks([])
|
||||
self._wave.clear_selection()
|
||||
self._wave.set_playhead(None)
|
||||
self._wave.set_markers(self._healed_seams.get(path, []))
|
||||
self._btn_undo.setEnabled(self._ver_idx > 0)
|
||||
self._btn_redo.setEnabled(self._ver_idx < len(self._versions) - 1)
|
||||
|
||||
@@ -4411,6 +4473,9 @@ class AudioEditorDialog(QDialog):
|
||||
|
||||
def _prepare_join_preview(self, seam_t: float) -> None:
|
||||
self._join_preview = (max(0.0, seam_t - 1.0), seam_t + 1.0)
|
||||
self._healed_seams[self._current()] = [seam_t]
|
||||
self._wave.set_markers([seam_t])
|
||||
self._set_status(f"Healed join marked at {seam_t:.2f}s")
|
||||
|
||||
def _on_heal_cut(self):
|
||||
self._apply_op(build_audio_heal_delete_command, whole_clip_ok=False,
|
||||
@@ -4443,15 +4508,20 @@ class AudioEditorDialog(QDialog):
|
||||
if not checked:
|
||||
self._stop_play()
|
||||
return
|
||||
play_t = self._playhead_secs()
|
||||
se = self._selection_secs()
|
||||
if se is not None:
|
||||
self._play_current(*se)
|
||||
if se is not None and se[0] <= play_t <= se[1]:
|
||||
self._play_current(se[0], se[1], loop=True)
|
||||
else:
|
||||
self._play_current()
|
||||
end = self._clip_dur if self._clip_dur > play_t else None
|
||||
self._play_current(play_t, end, loop=False)
|
||||
|
||||
def _play_args(self, start: float | None = None,
|
||||
end: float | None = None) -> list[str]:
|
||||
end: float | None = None,
|
||||
loop: bool = False) -> list[str]:
|
||||
args = ["-autoexit", "-nodisp", "-loglevel", "error"]
|
||||
if loop:
|
||||
args += ["-loop", "0"]
|
||||
if start is not None:
|
||||
args += ["-ss", str(start)]
|
||||
if end is not None and start is not None and end > start:
|
||||
@@ -4466,14 +4536,16 @@ class AudioEditorDialog(QDialog):
|
||||
return args
|
||||
|
||||
def _play_current(self, start: float | None = None,
|
||||
end: float | None = None) -> None:
|
||||
end: float | None = None,
|
||||
loop: bool = False) -> None:
|
||||
from PyQt6.QtCore import QProcess
|
||||
self._stop_play()
|
||||
self._play_proc = QProcess(self)
|
||||
self._play_proc.finished.connect(self._on_play_finished)
|
||||
self._play_proc.errorOccurred.connect(self._on_play_error)
|
||||
args = self._play_args(start, end)
|
||||
args = self._play_args(start, end, loop=loop)
|
||||
self._play_start = max(0.0, start if start is not None else 0.0)
|
||||
self._play_loop = loop
|
||||
if end is not None and end > self._play_start:
|
||||
self._play_end = end
|
||||
elif self._clip_dur > 0:
|
||||
@@ -4496,6 +4568,18 @@ class AudioEditorDialog(QDialog):
|
||||
return
|
||||
self._play_current(*self._join_preview)
|
||||
|
||||
def _on_wave_playhead_changed(self, t: float) -> None:
|
||||
if self._play_proc is not None:
|
||||
self._stop_play()
|
||||
|
||||
def _playhead_secs(self) -> float:
|
||||
t = self._wave.playhead()
|
||||
if t is None:
|
||||
t = 0.0
|
||||
if self._clip_dur > 0:
|
||||
t = max(0.0, min(t, self._clip_dur))
|
||||
return t
|
||||
|
||||
def _set_playback_speed(self, speed: float) -> None:
|
||||
if speed == 2.0 and self._btn_speed2.isChecked():
|
||||
self._btn_speed4.setChecked(False)
|
||||
@@ -4511,7 +4595,7 @@ class AudioEditorDialog(QDialog):
|
||||
return
|
||||
self._play_speed = eff
|
||||
if self._play_proc is not None:
|
||||
self._play_current(self._play_start, self._play_end)
|
||||
self._play_current(self._play_start, self._play_end, loop=self._play_loop)
|
||||
|
||||
def _tick_playhead(self) -> None:
|
||||
elapsed = self._play_elapsed.elapsed() / 1000.0
|
||||
@@ -4531,7 +4615,6 @@ class AudioEditorDialog(QDialog):
|
||||
proc = self._play_proc
|
||||
self._play_proc = None
|
||||
self._playhead_timer.stop()
|
||||
self._wave.set_playhead(None)
|
||||
if proc is not None:
|
||||
proc.deleteLater()
|
||||
self._btn_play.setText("▶ Play")
|
||||
@@ -4544,7 +4627,6 @@ class AudioEditorDialog(QDialog):
|
||||
proc = self._play_proc
|
||||
self._play_proc = None
|
||||
self._playhead_timer.stop()
|
||||
self._wave.set_playhead(None)
|
||||
if proc is not None:
|
||||
try:
|
||||
proc.finished.disconnect()
|
||||
|
||||
Reference in New Issue
Block a user