Tired and Poor and Old - Radio Panel v0.1.1
This commit is contained in:
+138
-22
@@ -4,14 +4,96 @@ from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QPushButton, QFrame, QSizePolicy
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer, Signal
|
||||
from PySide6.QtGui import QFont, QColor
|
||||
from PySide6.QtCore import Qt, QTimer, Signal, QRect
|
||||
from PySide6.QtGui import QFont, QColor, QPainter, QPen
|
||||
import numpy as np
|
||||
import os
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# LCD DISPLAY WIDGET
|
||||
# ─────────────────────────────────────────
|
||||
class WaveformWidget(QFrame):
|
||||
"""Displays downsampled waveform with playhead position."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedHeight(60)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #050505;
|
||||
border: 1px solid #1a1a1a;
|
||||
border-radius: 3px;
|
||||
}
|
||||
""")
|
||||
|
||||
self.waveform_data = None # Shape: (width,) - RMS per pixel
|
||||
self.position_pct = 0.0 # 0.0 to 1.0
|
||||
|
||||
def set_waveform(self, audio: np.ndarray):
|
||||
"""
|
||||
Downsample audio to fit widget width.
|
||||
audio shape: (2, samples) stereo
|
||||
"""
|
||||
if audio is None or audio.shape[1] == 0:
|
||||
self.waveform_data = None
|
||||
self.update()
|
||||
return
|
||||
|
||||
# Mix to mono
|
||||
mono = np.mean(audio, axis=0)
|
||||
|
||||
# Downsample to widget width
|
||||
width = self.width()
|
||||
if width < 10:
|
||||
width = 500 # default before widget is shown
|
||||
|
||||
samples_per_pixel = max(1, len(mono) // width)
|
||||
num_pixels = len(mono) // samples_per_pixel
|
||||
|
||||
self.waveform_data = np.array([
|
||||
np.sqrt(np.mean(mono[i*samples_per_pixel:(i+1)*samples_per_pixel]**2))
|
||||
for i in range(num_pixels)
|
||||
], dtype=np.float32)
|
||||
|
||||
self.update()
|
||||
|
||||
def set_position(self, position_pct: float):
|
||||
"""Update playhead position (0.0 to 1.0)."""
|
||||
self.position_pct = max(0.0, min(1.0, position_pct))
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
super().paintEvent(event)
|
||||
|
||||
if self.waveform_data is None or len(self.waveform_data) == 0:
|
||||
return
|
||||
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
rect = self.rect()
|
||||
width = rect.width()
|
||||
height = rect.height()
|
||||
|
||||
# Scale waveform to fit height
|
||||
max_val = np.max(self.waveform_data) if np.max(self.waveform_data) > 0 else 1.0
|
||||
scaled = (self.waveform_data / max_val) * (height * 0.8)
|
||||
|
||||
# Draw waveform bars
|
||||
pen = QPen(QColor("#00ff41"), 1)
|
||||
painter.setPen(pen)
|
||||
|
||||
x_scale = width / len(self.waveform_data)
|
||||
for i, rms in enumerate(self.waveform_data):
|
||||
x = int(i * x_scale)
|
||||
bar_h = int(scaled[i])
|
||||
y_top = (height - bar_h) // 2
|
||||
painter.drawLine(x, y_top, x, y_top + bar_h)
|
||||
|
||||
# Draw playhead
|
||||
playhead_x = int(self.position_pct * width)
|
||||
pen = QPen(QColor("#ffffff"), 2)
|
||||
painter.setPen(pen)
|
||||
painter.drawLine(playhead_x, 0, playhead_x, height)
|
||||
|
||||
|
||||
class LCDDisplay(QFrame):
|
||||
def __init__(self, parent=None):
|
||||
@@ -107,10 +189,6 @@ class LCDDisplay(QFrame):
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# TRANSPORT BUTTON HELPER
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
def make_transport_button(text: str,
|
||||
color: str = "#3a3a3a",
|
||||
text_color: str = "white",
|
||||
@@ -137,12 +215,8 @@ def make_transport_button(text: str,
|
||||
return btn
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# DECK WIDGET
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class DeckWidget(QWidget):
|
||||
track_started = Signal(str) # emits filepath when playback begins
|
||||
track_started = Signal(str)
|
||||
|
||||
def __init__(self, deck_key: str, label: str, accent_color: str,
|
||||
engine, parent=None):
|
||||
@@ -150,6 +224,7 @@ class DeckWidget(QWidget):
|
||||
self.deck_key = deck_key
|
||||
self.engine = engine
|
||||
self.accent_color = accent_color
|
||||
self.current_theme = None
|
||||
|
||||
self._build_ui(label)
|
||||
self._connect_signals()
|
||||
@@ -186,6 +261,10 @@ class DeckWidget(QWidget):
|
||||
self.lcd = LCDDisplay()
|
||||
root.addWidget(self.lcd)
|
||||
|
||||
# Waveform display
|
||||
self.waveform = WaveformWidget()
|
||||
root.addWidget(self.waveform)
|
||||
|
||||
transport = QHBoxLayout()
|
||||
transport.setSpacing(6)
|
||||
|
||||
@@ -206,6 +285,31 @@ class DeckWidget(QWidget):
|
||||
|
||||
root.addLayout(transport)
|
||||
|
||||
def apply_theme(self, theme: dict):
|
||||
"""Apply theme colours to deck widget borders and LCD."""
|
||||
self.current_theme = theme
|
||||
self.setStyleSheet(f"""
|
||||
QWidget {{
|
||||
background-color: {theme['bg2']};
|
||||
border: 2px solid {theme['border']};
|
||||
border-radius: 6px;
|
||||
}}
|
||||
""")
|
||||
self.lcd.setStyleSheet(f"""
|
||||
QFrame {{
|
||||
background-color: {theme['bg']};
|
||||
border: 2px solid {theme['border']};
|
||||
border-radius: 4px;
|
||||
}}
|
||||
""")
|
||||
self.waveform.setStyleSheet(f"""
|
||||
QFrame {{
|
||||
background-color: {theme['bg']};
|
||||
border: 1px solid {theme['border']};
|
||||
border-radius: 3px;
|
||||
}}
|
||||
""")
|
||||
|
||||
def _connect_signals(self):
|
||||
self.btn_play.clicked.connect(self._toggle_play)
|
||||
self.btn_stop.clicked.connect(self._stop_eject)
|
||||
@@ -214,18 +318,18 @@ class DeckWidget(QWidget):
|
||||
self.btn_back_15.clicked.connect(lambda: self._scrub(-15))
|
||||
self.btn_fwd_15.clicked.connect(lambda: self._scrub(15))
|
||||
|
||||
# ── PUBLIC — called by playlist ───────────────────────────────────────
|
||||
|
||||
def set_track_name(self, name: str):
|
||||
self.lcd.update_track(name)
|
||||
self.lcd.update_status("LOADED")
|
||||
self.lcd.update_time("00:00", "-00:00")
|
||||
# Build waveform when track loads
|
||||
deck = self.engine.decks[self.deck_key]
|
||||
if deck.track:
|
||||
self.waveform.set_waveform(deck.track.audio)
|
||||
|
||||
def set_queue_name(self, name: str):
|
||||
self.lcd.update_queue(name)
|
||||
|
||||
# ── PUBLIC MIDI METHODS ───────────────────────────────────────────────
|
||||
|
||||
def toggle_play(self):
|
||||
self._toggle_play()
|
||||
|
||||
@@ -252,8 +356,6 @@ class DeckWidget(QWidget):
|
||||
def scrub(self, delta: float):
|
||||
self._scrub(delta)
|
||||
|
||||
# ── ACTIONS ───────────────────────────────────────────────────────────
|
||||
|
||||
def _toggle_play(self):
|
||||
deck = self.engine.decks[self.deck_key]
|
||||
if deck.track is None:
|
||||
@@ -306,6 +408,7 @@ class DeckWidget(QWidget):
|
||||
self.lcd.update_time("00:00", "-00:00")
|
||||
self.lcd.update_status("STOPPED")
|
||||
self.lcd.set_playing(False)
|
||||
self.waveform.set_waveform(None)
|
||||
|
||||
def _go_to_start(self):
|
||||
self.engine.decks[self.deck_key].go_to_start()
|
||||
@@ -316,11 +419,21 @@ class DeckWidget(QWidget):
|
||||
def _scrub(self, seconds: float):
|
||||
self.engine.decks[self.deck_key].skip_seconds(seconds)
|
||||
|
||||
# ── DISPLAY REFRESH ───────────────────────────────────────────────────
|
||||
|
||||
def _refresh_display(self):
|
||||
deck = self.engine.decks[self.deck_key]
|
||||
|
||||
# Auto-load queued track when current finishes
|
||||
if not deck.playing and deck.track is not None:
|
||||
if deck.position >= deck.track.num_samples - 1:
|
||||
if deck.queued:
|
||||
deck.load(deck.queued)
|
||||
self.lcd.update_track(os.path.splitext(os.path.basename(deck.queued.filepath))[0])
|
||||
self.waveform.set_waveform(deck.track.audio)
|
||||
deck.queued = None
|
||||
self.lcd.update_queue("")
|
||||
self.lcd.update_status("LOADED")
|
||||
|
||||
# Sync button if stopped manually
|
||||
if not deck.playing and self.btn_play.text() == "⏸":
|
||||
self.btn_play.setText("▶")
|
||||
self.btn_play.setStyleSheet("""
|
||||
@@ -340,6 +453,9 @@ class DeckWidget(QWidget):
|
||||
deck.get_elapsed(),
|
||||
deck.get_remaining()
|
||||
)
|
||||
# Update waveform position
|
||||
position_pct = deck.position / deck.track.num_samples
|
||||
self.waveform.set_position(position_pct)
|
||||
|
||||
if deck.queued:
|
||||
self.lcd.update_queue(
|
||||
|
||||
Reference in New Issue
Block a user