124 lines
4.9 KiB
Python
124 lines
4.9 KiB
Python
"""Waveform display: envelope, playhead, click-to-seek, drag-to-select (trim region)."""
|
|
import numpy as np
|
|
from PySide6.QtCore import QPointF, QRectF, Qt, QTimer, Signal
|
|
from PySide6.QtGui import QColor, QPainter, QPalette, QPen
|
|
from PySide6.QtWidgets import QSizePolicy, QWidget
|
|
|
|
from . import spinner
|
|
|
|
|
|
class Waveform(QWidget):
|
|
seekRequested = Signal(float) # seconds
|
|
selectionChanged = Signal(float, float) # seconds (start, end); (0, 0) = none
|
|
|
|
def __init__(self, parent=None, selectable=False):
|
|
super().__init__(parent)
|
|
self.setMinimumHeight(70)
|
|
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
|
self.selectable = selectable
|
|
self.env = None # (mins, maxs) at a fixed resolution
|
|
self.duration = 0.0
|
|
self.playhead = None
|
|
self.sel = None
|
|
self._drag = None
|
|
self.placeholder = "no audio"
|
|
self.accent = None
|
|
self.bg = None # background colour (theme), else the palette's Base
|
|
self.busy = False # spinner beside the placeholder (audio being loaded)
|
|
self._angle = 0
|
|
self._timer = QTimer(self)
|
|
self._timer.setInterval(spinner.STEP_MS)
|
|
self._timer.timeout.connect(self._spin)
|
|
|
|
def _spin(self):
|
|
self._angle = (self._angle + spinner.STEP_DEG) % 360
|
|
self.update()
|
|
|
|
def set_audio(self, x, rate, placeholder="no audio", busy=False):
|
|
self.placeholder = placeholder
|
|
self.busy = busy and (x is None or len(x) == 0)
|
|
self._timer.start() if self.busy else self._timer.stop()
|
|
self.sel = None
|
|
self.playhead = None
|
|
if x is None or len(x) == 0:
|
|
self.env, self.duration = None, 0.0
|
|
else:
|
|
m = x.mean(axis=1) if x.ndim == 2 else x
|
|
cols = 2000
|
|
n = len(m)
|
|
edges = np.linspace(0, n, cols + 1).astype(int)
|
|
mins = np.array([m[a:b].min() if b > a else 0 for a, b in zip(edges[:-1], edges[1:])])
|
|
maxs = np.array([m[a:b].max() if b > a else 0 for a, b in zip(edges[:-1], edges[1:])])
|
|
self.env = (mins, maxs)
|
|
self.duration = n / float(rate)
|
|
self.update()
|
|
|
|
def set_playhead(self, seconds):
|
|
self.playhead = seconds
|
|
self.update()
|
|
|
|
def set_selection(self, a, b):
|
|
self.sel = (min(a, b), max(a, b)) if b != a else None
|
|
self.update()
|
|
|
|
def _x2t(self, x):
|
|
return max(0.0, min(self.duration, x / max(1, self.width()) * self.duration))
|
|
|
|
def _t2x(self, t):
|
|
return t / self.duration * self.width() if self.duration else 0
|
|
|
|
def mousePressEvent(self, e):
|
|
if not self.duration:
|
|
return
|
|
t = self._x2t(e.position().x())
|
|
if self.selectable and e.button() == Qt.MouseButton.LeftButton and e.modifiers() & Qt.KeyboardModifier.ShiftModifier:
|
|
self._drag = t
|
|
elif self.selectable and e.button() == Qt.MouseButton.RightButton:
|
|
self._drag = t
|
|
else:
|
|
self.seekRequested.emit(t)
|
|
|
|
def mouseMoveEvent(self, e):
|
|
if self._drag is not None:
|
|
self.set_selection(self._drag, self._x2t(e.position().x()))
|
|
|
|
def mouseReleaseEvent(self, e):
|
|
if self._drag is not None:
|
|
self._drag = None
|
|
a, b = self.sel if self.sel else (0.0, 0.0)
|
|
self.selectionChanged.emit(a, b)
|
|
|
|
def paintEvent(self, _):
|
|
p = QPainter(self)
|
|
pal = self.palette()
|
|
r = self.rect()
|
|
p.fillRect(r, self.bg or pal.color(QPalette.ColorRole.Base))
|
|
mid = r.height() / 2
|
|
fg = self.accent or pal.color(QPalette.ColorRole.Highlight)
|
|
if self.env is None:
|
|
p.setPen(pal.color(QPalette.ColorRole.PlaceholderText))
|
|
p.drawText(r, Qt.AlignmentFlag.AlignCenter, self.placeholder)
|
|
if self.busy:
|
|
tw = p.fontMetrics().horizontalAdvance(self.placeholder)
|
|
d = 14
|
|
spinner.paint_arc(p, QRectF(r.center().x() - tw / 2 - d - 8, mid - d / 2, d, d), self._angle)
|
|
return
|
|
if self.sel:
|
|
sc = QColor(fg)
|
|
sc.setAlpha(50)
|
|
p.fillRect(QRectF(self._t2x(self.sel[0]), 0, self._t2x(self.sel[1]) - self._t2x(self.sel[0]), r.height()), sc)
|
|
mins, maxs = self.env
|
|
w = max(1, r.width())
|
|
idx = (np.arange(w) * len(mins) / w).astype(int)
|
|
p.setPen(QPen(fg, 1))
|
|
for x, i in enumerate(idx):
|
|
j = min(len(mins), int((x + 1) * len(mins) / w) + 1)
|
|
lo, hi = mins[i:j].min(), maxs[i:j].max()
|
|
p.drawLine(QPointF(x, mid - hi * (mid - 2)), QPointF(x, mid - lo * (mid - 2)))
|
|
p.setPen(QPen(pal.color(QPalette.ColorRole.Mid), 1))
|
|
p.drawLine(0, int(mid), w, int(mid))
|
|
if self.playhead is not None:
|
|
p.setPen(QPen(pal.color(QPalette.ColorRole.Text), 1))
|
|
x = self._t2x(self.playhead)
|
|
p.drawLine(QPointF(x, 0), QPointF(x, r.height()))
|