feat: waveform view/selection state + band/handle painting
This commit is contained in:
@@ -3946,29 +3946,70 @@ class AudioWaveform(QWidget):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._peaks: list[float] = []
|
self._peaks: list[float] = []
|
||||||
self.setFixedHeight(48)
|
self._view_start = 0.0
|
||||||
|
self._view_dur = 0.0
|
||||||
|
self._sel: tuple[float, float] | None = None
|
||||||
|
self._playhead: float | None = None
|
||||||
|
self.setFixedHeight(96)
|
||||||
self.setToolTip("Waveform of the current audio area (↻ to refresh)")
|
self.setToolTip("Waveform of the current audio area (↻ to refresh)")
|
||||||
|
|
||||||
def set_peaks(self, peaks: list[float]) -> None:
|
def set_peaks(self, peaks: list[float]) -> None:
|
||||||
self._peaks = list(peaks or [])
|
self._peaks = list(peaks or [])
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
def set_view(self, start: float, dur: float) -> None:
|
||||||
|
self._view_start = start
|
||||||
|
self._view_dur = dur
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def set_selection(self, start: float, end: float) -> None:
|
||||||
|
if self._view_dur > 0:
|
||||||
|
lo = self._view_start
|
||||||
|
hi = self._view_start + self._view_dur
|
||||||
|
start = min(max(start, lo), hi)
|
||||||
|
end = min(max(end, lo), hi)
|
||||||
|
if start > end:
|
||||||
|
start, end = end, start
|
||||||
|
self._sel = (start, end)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def selection(self) -> tuple[float, float] | None:
|
||||||
|
return self._sel
|
||||||
|
|
||||||
|
def set_playhead(self, t: float | None) -> None:
|
||||||
|
self._playhead = t
|
||||||
|
self.update()
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
self._peaks = []
|
self._peaks = []
|
||||||
|
self._sel = None
|
||||||
|
self._playhead = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def paintEvent(self, _ev):
|
def paintEvent(self, _ev):
|
||||||
|
from core.waveform import t_to_x
|
||||||
p = QPainter(self)
|
p = QPainter(self)
|
||||||
p.fillRect(self.rect(), QColor(30, 30, 30))
|
p.fillRect(self.rect(), QColor(30, 30, 30))
|
||||||
if not self._peaks:
|
|
||||||
return
|
|
||||||
w = self.width(); h = self.height(); mid = h / 2
|
w = self.width(); h = self.height(); mid = h / 2
|
||||||
n = len(self._peaks)
|
if self._peaks:
|
||||||
p.setPen(QPen(QColor(0, 220, 190)))
|
n = len(self._peaks)
|
||||||
for i, v in enumerate(self._peaks):
|
p.setPen(QPen(QColor(0, 220, 190)))
|
||||||
x = int(i * w / n)
|
for i, v in enumerate(self._peaks):
|
||||||
bar_h = v * (h - 4) / 2
|
x = int(i * w / n)
|
||||||
p.drawLine(x, int(mid - bar_h), x, int(mid + bar_h))
|
bar_h = v * (h - 4) / 2
|
||||||
|
p.drawLine(x, int(mid - bar_h), x, int(mid + bar_h))
|
||||||
|
if self._sel and self._view_dur > 0:
|
||||||
|
x1 = t_to_x(self._sel[0], w, self._view_start, self._view_dur)
|
||||||
|
x2 = t_to_x(self._sel[1], w, self._view_start, self._view_dur)
|
||||||
|
p.fillRect(QRect(x1, 0, max(1, x2 - x1), h), QColor(0, 200, 180, 50))
|
||||||
|
p.setPen(QPen(QColor(0, 220, 190), 2))
|
||||||
|
p.drawLine(x1, 0, x1, h)
|
||||||
|
p.drawLine(x2, 0, x2, 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)
|
||||||
|
p.setPen(QPen(QColor(255, 255, 255, 160), 1))
|
||||||
|
p.drawLine(px, 0, px, h)
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
|
|||||||
@@ -398,6 +398,22 @@ def test_waveform_strip_present_and_safe(win):
|
|||||||
win._on_wave_refresh()
|
win._on_wave_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
def test_waveform_view_and_selection(win):
|
||||||
|
w = win._wave
|
||||||
|
w.set_view(10.0, 4.0)
|
||||||
|
w.set_selection(11.0, 12.5)
|
||||||
|
assert w._view_start == 10.0 and w._view_dur == 4.0
|
||||||
|
assert w.selection() == (11.0, 12.5)
|
||||||
|
# selection clamps into the view window and keeps start < end
|
||||||
|
w.set_selection(9.0, 20.0)
|
||||||
|
s, e = w.selection()
|
||||||
|
assert s >= 10.0 and e <= 14.0 and s < e
|
||||||
|
w.set_peaks([0.1, 0.9, 0.3]) # still paints without crashing
|
||||||
|
w.set_playhead(11.5)
|
||||||
|
w.clear() # resets display state
|
||||||
|
assert w.selection() is None
|
||||||
|
|
||||||
|
|
||||||
def test_audition_button_present_and_safe(win):
|
def test_audition_button_present_and_safe(win):
|
||||||
from PyQt6.QtWidgets import QPushButton
|
from PyQt6.QtWidgets import QPushButton
|
||||||
assert isinstance(win._btn_audio_play, QPushButton)
|
assert isinstance(win._btn_audio_play, QPushButton)
|
||||||
|
|||||||
Reference in New Issue
Block a user