fix: make audio editor playback local

This commit is contained in:
2026-07-04 21:03:08 +02:00
parent fe95076195
commit ccad721c98
2 changed files with 195 additions and 10 deletions
+99 -10
View File
@@ -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()