fix: make audio editor playback local
This commit is contained in:
@@ -22,7 +22,10 @@ from PyQt6.QtWidgets import (
|
||||
QToolBox, QGroupBox,
|
||||
QGridLayout,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QObject, QThread, QTimer, QRect, QSize, pyqtSignal, QSettings
|
||||
from PyQt6.QtCore import (
|
||||
Qt, QObject, QThread, QTimer, QElapsedTimer, QRect, QSize, pyqtSignal,
|
||||
QSettings,
|
||||
)
|
||||
from PyQt6.QtGui import QPainter, QColor, QPen, QPixmap, QDragEnterEvent, QDropEvent, QCursor, QFont, QKeySequence, QShortcut, QIcon
|
||||
if sys.platform == "win32":
|
||||
# Help ctypes find libmpv-2.dll next to main.py or in frozen bundle
|
||||
@@ -4239,6 +4242,7 @@ class AudioEditorDialog(QDialog):
|
||||
version files."""
|
||||
|
||||
_EDIT_DECODE_CAP = 600.0 # seconds of audio to decode for the waveform
|
||||
_EDITOR_WAVEFORM_BINS = 2000
|
||||
|
||||
def __init__(self, path: str, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -4249,6 +4253,13 @@ class AudioEditorDialog(QDialog):
|
||||
self._temps: set[str] = set() # rendered version temps to clean up
|
||||
self._last_saved = None # path of the last Save-as (for library)
|
||||
self._play_proc = None # ffplay audition process
|
||||
self._play_speed = 1.0
|
||||
self._play_start = 0.0
|
||||
self._play_end: float | None = None
|
||||
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
|
||||
|
||||
@@ -4263,6 +4274,14 @@ class AudioEditorDialog(QDialog):
|
||||
self._btn_loop_join = QPushButton("Loop Join")
|
||||
self._btn_play = QPushButton("▶ Play")
|
||||
self._btn_play.setCheckable(True)
|
||||
self._btn_speed2 = QPushButton("x2")
|
||||
self._btn_speed2.setCheckable(True)
|
||||
self._btn_speed2.setFixedWidth(32)
|
||||
self._btn_speed2.setToolTip("Editor playback at 2x speed")
|
||||
self._btn_speed4 = QPushButton("x4")
|
||||
self._btn_speed4.setCheckable(True)
|
||||
self._btn_speed4.setFixedWidth(32)
|
||||
self._btn_speed4.setToolTip("Editor playback at 4x speed")
|
||||
self._btn_save_library = QPushButton("Save to Library")
|
||||
self._btn_save_as = QPushButton("Save as…")
|
||||
self._btn_close = QPushButton("Close")
|
||||
@@ -4275,6 +4294,8 @@ class AudioEditorDialog(QDialog):
|
||||
(self._btn_redo, self._on_redo),
|
||||
(self._btn_loop_join, self._on_loop_join),
|
||||
(self._btn_play, self._on_play),
|
||||
(self._btn_speed2, lambda: self._set_playback_speed(2.0)),
|
||||
(self._btn_speed4, lambda: self._set_playback_speed(4.0)),
|
||||
(self._btn_save_library, self._on_save_to_library),
|
||||
(self._btn_save_as, self._on_save_as),
|
||||
(self._btn_close, self.close)):
|
||||
@@ -4292,7 +4313,7 @@ class AudioEditorDialog(QDialog):
|
||||
outer.addWidget(self._status)
|
||||
bottom = QHBoxLayout()
|
||||
for b in (self._btn_undo, self._btn_redo, self._btn_loop_join,
|
||||
self._btn_play):
|
||||
self._btn_play, self._btn_speed2, self._btn_speed4):
|
||||
bottom.addWidget(b)
|
||||
bottom.addStretch()
|
||||
bottom.addWidget(self._btn_save_library)
|
||||
@@ -4300,6 +4321,17 @@ class AudioEditorDialog(QDialog):
|
||||
bottom.addWidget(self._btn_close)
|
||||
outer.addLayout(bottom)
|
||||
|
||||
self._play_shortcuts = []
|
||||
for key in ("Space", "P"):
|
||||
sc = QShortcut(QKeySequence(key), self)
|
||||
sc.setContext(Qt.ShortcutContext.WindowShortcut)
|
||||
sc.activated.connect(self._btn_play.click)
|
||||
self._play_shortcuts.append(sc)
|
||||
sc_stop = QShortcut(QKeySequence("K"), self)
|
||||
sc_stop.setContext(Qt.ShortcutContext.WindowShortcut)
|
||||
sc_stop.activated.connect(self._stop_play)
|
||||
self._play_shortcuts.append(sc_stop)
|
||||
|
||||
self._reload()
|
||||
|
||||
def _current(self) -> str:
|
||||
@@ -4314,7 +4346,7 @@ class AudioEditorDialog(QDialog):
|
||||
if dur > 0:
|
||||
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))
|
||||
self._wave.set_peaks(peaks(samples, buckets=self._EDITOR_WAVEFORM_BINS))
|
||||
self._wave.set_selection(0.0, dur)
|
||||
else:
|
||||
self._wave.set_view(0.0, 0.0)
|
||||
@@ -4411,7 +4443,27 @@ class AudioEditorDialog(QDialog):
|
||||
if not checked:
|
||||
self._stop_play()
|
||||
return
|
||||
self._play_current()
|
||||
se = self._selection_secs()
|
||||
if se is not None:
|
||||
self._play_current(*se)
|
||||
else:
|
||||
self._play_current()
|
||||
|
||||
def _play_args(self, start: float | None = None,
|
||||
end: float | None = None) -> list[str]:
|
||||
args = ["-autoexit", "-nodisp", "-loglevel", "error"]
|
||||
if start is not None:
|
||||
args += ["-ss", str(start)]
|
||||
if end is not None and start is not None and end > start:
|
||||
args += ["-t", str(end - start)]
|
||||
if self._play_speed > 1.0:
|
||||
if self._play_speed == 4.0:
|
||||
tempo = "atempo=2.0,atempo=2.0"
|
||||
else:
|
||||
tempo = f"atempo={self._play_speed:.1f}"
|
||||
args += ["-af", tempo]
|
||||
args.append(self._current())
|
||||
return args
|
||||
|
||||
def _play_current(self, start: float | None = None,
|
||||
end: float | None = None) -> None:
|
||||
@@ -4420,12 +4472,17 @@ class AudioEditorDialog(QDialog):
|
||||
self._play_proc = QProcess(self)
|
||||
self._play_proc.finished.connect(self._on_play_finished)
|
||||
self._play_proc.errorOccurred.connect(self._on_play_error)
|
||||
args = ["-autoexit", "-nodisp", "-loglevel", "error"]
|
||||
if start is not None:
|
||||
args += ["-ss", str(start)]
|
||||
if end is not None and start is not None and end > start:
|
||||
args += ["-t", str(end - start)]
|
||||
args.append(self._current())
|
||||
args = self._play_args(start, end)
|
||||
self._play_start = max(0.0, start if start is not None else 0.0)
|
||||
if end is not None and end > self._play_start:
|
||||
self._play_end = end
|
||||
elif self._clip_dur > 0:
|
||||
self._play_end = self._clip_dur
|
||||
else:
|
||||
self._play_end = None
|
||||
self._wave.set_playhead(self._play_start)
|
||||
self._play_elapsed.restart()
|
||||
self._playhead_timer.start()
|
||||
self._play_proc.start(_bin("ffplay"), args)
|
||||
self._btn_play.setText("■ Stop")
|
||||
if not self._btn_play.isChecked():
|
||||
@@ -4439,6 +4496,30 @@ class AudioEditorDialog(QDialog):
|
||||
return
|
||||
self._play_current(*self._join_preview)
|
||||
|
||||
def _set_playback_speed(self, speed: float) -> None:
|
||||
if speed == 2.0 and self._btn_speed2.isChecked():
|
||||
self._btn_speed4.setChecked(False)
|
||||
elif speed == 4.0 and self._btn_speed4.isChecked():
|
||||
self._btn_speed2.setChecked(False)
|
||||
if self._btn_speed4.isChecked():
|
||||
eff = 4.0
|
||||
elif self._btn_speed2.isChecked():
|
||||
eff = 2.0
|
||||
else:
|
||||
eff = 1.0
|
||||
if eff == self._play_speed:
|
||||
return
|
||||
self._play_speed = eff
|
||||
if self._play_proc is not None:
|
||||
self._play_current(self._play_start, self._play_end)
|
||||
|
||||
def _tick_playhead(self) -> None:
|
||||
elapsed = self._play_elapsed.elapsed() / 1000.0
|
||||
t = self._play_start + elapsed * self._play_speed
|
||||
if self._play_end is not None:
|
||||
t = min(t, self._play_end)
|
||||
self._wave.set_playhead(t)
|
||||
|
||||
def _on_play_error(self, _err) -> None:
|
||||
self._set_status("Playback unavailable (ffplay not found)")
|
||||
self._teardown_play()
|
||||
@@ -4449,6 +4530,8 @@ class AudioEditorDialog(QDialog):
|
||||
def _teardown_play(self) -> None:
|
||||
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")
|
||||
@@ -4460,6 +4543,8 @@ class AudioEditorDialog(QDialog):
|
||||
def _stop_play(self) -> None:
|
||||
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()
|
||||
@@ -4470,6 +4555,10 @@ class AudioEditorDialog(QDialog):
|
||||
proc.waitForFinished(100)
|
||||
proc.deleteLater()
|
||||
self._btn_play.setText("▶ Play")
|
||||
if self._btn_play.isChecked():
|
||||
self._btn_play.blockSignals(True)
|
||||
self._btn_play.setChecked(False)
|
||||
self._btn_play.blockSignals(False)
|
||||
|
||||
def _cleanup(self) -> None:
|
||||
self._stop_play()
|
||||
|
||||
Reference in New Issue
Block a user