2026-04-29 16:21:44 +10:00
|
|
|
# deck_widget.py
|
|
|
|
|
|
|
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
|
|
|
QPushButton, QFrame, QSizePolicy
|
|
|
|
|
)
|
2026-05-08 18:33:01 +10:00
|
|
|
from PySide6.QtCore import Qt, QTimer, Signal, QSize
|
2026-04-30 00:30:23 +10:00
|
|
|
from PySide6.QtGui import QFont, QColor, QPainter, QPen
|
|
|
|
|
import numpy as np
|
2026-04-29 16:21:44 +10:00
|
|
|
import os
|
|
|
|
|
|
2026-05-08 18:33:01 +10:00
|
|
|
from icons import (
|
|
|
|
|
icon_play, icon_pause, icon_stop, icon_skip_start,
|
|
|
|
|
icon_skip_end, icon_skip_back, icon_skip_forward, icon_eject
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
self.waveform_data = None
|
|
|
|
|
self.position_pct = 0.0
|
|
|
|
|
self.warning_flash = False
|
2026-04-30 00:30:23 +10:00
|
|
|
|
|
|
|
|
def set_waveform(self, audio: np.ndarray):
|
|
|
|
|
if audio is None or audio.shape[1] == 0:
|
|
|
|
|
self.waveform_data = None
|
|
|
|
|
self.update()
|
|
|
|
|
return
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
mono = np.mean(audio, axis=0)
|
|
|
|
|
width = max(500, self.width())
|
2026-04-30 00:30:23 +10:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
def set_position(self, position_pct: float, warning: bool = False):
|
|
|
|
|
self.position_pct = max(0.0, min(1.0, position_pct))
|
|
|
|
|
self.warning_flash = warning
|
2026-04-30 00:30:23 +10:00
|
|
|
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)
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
rect = self.rect().adjusted(2, 2, -2, -2)
|
2026-04-30 00:30:23 +10:00
|
|
|
width = rect.width()
|
|
|
|
|
height = rect.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)
|
|
|
|
|
|
|
|
|
|
x_scale = width / len(self.waveform_data)
|
2026-05-08 17:19:11 +10:00
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
for i, rms in enumerate(self.waveform_data):
|
2026-05-08 17:19:11 +10:00
|
|
|
x = rect.x() + int(i * x_scale)
|
|
|
|
|
bar_h = int(scaled[i])
|
|
|
|
|
y_top = rect.y() + (height - bar_h) // 2
|
|
|
|
|
|
|
|
|
|
pct = i / len(self.waveform_data)
|
|
|
|
|
|
|
|
|
|
if pct < self.position_pct:
|
|
|
|
|
color = QColor("#004400")
|
|
|
|
|
elif pct > 0.85:
|
|
|
|
|
color = QColor("#ff0000") if self.warning_flash else QColor("#aa0000")
|
|
|
|
|
elif pct > 0.70:
|
|
|
|
|
color = QColor("#ffaa00")
|
|
|
|
|
else:
|
|
|
|
|
color = QColor("#00ff41")
|
|
|
|
|
|
|
|
|
|
pen = QPen(color, 1)
|
|
|
|
|
painter.setPen(pen)
|
2026-04-30 00:30:23 +10:00
|
|
|
painter.drawLine(x, y_top, x, y_top + bar_h)
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
playhead_x = rect.x() + int(self.position_pct * width)
|
2026-04-30 00:30:23 +10:00
|
|
|
pen = QPen(QColor("#ffffff"), 2)
|
|
|
|
|
painter.setPen(pen)
|
2026-05-08 17:19:11 +10:00
|
|
|
painter.drawLine(playhead_x, rect.y(), playhead_x, rect.y() + height)
|
2026-04-30 00:30:23 +10:00
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
class LCDDisplay(QFrame):
|
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self.setFrameStyle(QFrame.Box | QFrame.Sunken)
|
|
|
|
|
self.setStyleSheet("""
|
|
|
|
|
QFrame {
|
|
|
|
|
background-color: #0a1a0a;
|
|
|
|
|
border: 2px solid #1a2a1a;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
layout.setContentsMargins(10, 8, 10, 8)
|
|
|
|
|
layout.setSpacing(4)
|
|
|
|
|
|
|
|
|
|
self.track_label = QLabel("NO TRACK LOADED")
|
|
|
|
|
self.track_label.setFont(QFont("Courier New", 13, QFont.Bold))
|
|
|
|
|
self.track_label.setStyleSheet(
|
|
|
|
|
"color: #00ff41; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
|
|
|
self.track_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
|
|
|
|
|
|
|
|
time_row = QHBoxLayout()
|
|
|
|
|
time_row.setSpacing(4)
|
|
|
|
|
|
|
|
|
|
self.elapsed_label = QLabel("00:00")
|
|
|
|
|
self.elapsed_label.setFont(QFont("Courier New", 32, QFont.Bold))
|
|
|
|
|
self.elapsed_label.setStyleSheet(
|
|
|
|
|
"color: #00ff41; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
self.elapsed_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
|
|
|
|
|
|
|
|
self.remaining_label = QLabel("-00:00")
|
|
|
|
|
self.remaining_label.setFont(QFont("Courier New", 18, QFont.Bold))
|
|
|
|
|
self.remaining_label.setStyleSheet(
|
|
|
|
|
"color: #00cc33; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
|
|
|
|
|
|
|
|
time_row.addWidget(self.elapsed_label)
|
|
|
|
|
time_row.addStretch()
|
|
|
|
|
time_row.addWidget(self.remaining_label)
|
|
|
|
|
|
|
|
|
|
status_row = QHBoxLayout()
|
|
|
|
|
|
|
|
|
|
self.status_label = QLabel("STOPPED")
|
|
|
|
|
self.status_label.setFont(QFont("Courier New", 11, QFont.Bold))
|
|
|
|
|
self.status_label.setStyleSheet(
|
|
|
|
|
"color: #008822; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.queue_label = QLabel("QUEUE: EMPTY")
|
|
|
|
|
self.queue_label.setFont(QFont("Courier New", 10))
|
|
|
|
|
self.queue_label.setStyleSheet(
|
|
|
|
|
"color: #008822; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
self.queue_label.setAlignment(Qt.AlignRight)
|
|
|
|
|
|
|
|
|
|
status_row.addWidget(self.status_label)
|
|
|
|
|
status_row.addStretch()
|
|
|
|
|
status_row.addWidget(self.queue_label)
|
|
|
|
|
|
|
|
|
|
layout.addWidget(self.track_label)
|
|
|
|
|
layout.addLayout(time_row)
|
|
|
|
|
layout.addLayout(status_row)
|
|
|
|
|
|
|
|
|
|
def update_track(self, name: str):
|
|
|
|
|
if len(name) > 40:
|
|
|
|
|
name = name[:37] + "..."
|
|
|
|
|
self.track_label.setText(name)
|
|
|
|
|
|
|
|
|
|
def update_time(self, elapsed: str, remaining: str):
|
|
|
|
|
self.elapsed_label.setText(elapsed)
|
|
|
|
|
self.remaining_label.setText(remaining)
|
|
|
|
|
|
|
|
|
|
def update_status(self, status: str):
|
|
|
|
|
self.status_label.setText(status)
|
|
|
|
|
|
|
|
|
|
def update_queue(self, name: str):
|
|
|
|
|
if name:
|
|
|
|
|
short = name[:25] + "..." if len(name) > 25 else name
|
|
|
|
|
self.queue_label.setText(f"QUEUE: {short}")
|
|
|
|
|
else:
|
|
|
|
|
self.queue_label.setText("QUEUE: EMPTY")
|
|
|
|
|
|
|
|
|
|
def set_playing(self, playing: bool):
|
|
|
|
|
colour = "#00ff41" if playing else "#00aa2a"
|
|
|
|
|
self.elapsed_label.setStyleSheet(
|
|
|
|
|
f"color: {colour}; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
def flash_warning(self, active: bool):
|
|
|
|
|
"""Flash all time displays red when in warning zone."""
|
|
|
|
|
if active:
|
|
|
|
|
self.elapsed_label.setStyleSheet(
|
|
|
|
|
"color: #ff4444; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
self.remaining_label.setStyleSheet(
|
|
|
|
|
"color: #ff4444; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
self.elapsed_label.setStyleSheet(
|
|
|
|
|
"color: #00ff41; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
self.remaining_label.setStyleSheet(
|
|
|
|
|
"color: #00cc33; background: transparent;"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
|
2026-05-08 18:33:01 +10:00
|
|
|
def make_icon_button(icon: QIcon,
|
|
|
|
|
color: str = "#3a3a3a",
|
|
|
|
|
icon_color: str = "white",
|
|
|
|
|
width: int = 52,
|
|
|
|
|
height: int = 48) -> QPushButton:
|
|
|
|
|
btn = QPushButton()
|
2026-04-29 16:21:44 +10:00
|
|
|
btn.setFixedSize(width, height)
|
2026-05-08 18:33:01 +10:00
|
|
|
btn.setIconSize(QSize(22, 22))
|
|
|
|
|
btn.setIcon(icon)
|
2026-04-29 16:21:44 +10:00
|
|
|
btn.setStyleSheet(f"""
|
|
|
|
|
QPushButton {{
|
|
|
|
|
background-color: {color};
|
|
|
|
|
border: 1px solid #555;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}}
|
|
|
|
|
QPushButton:hover {{
|
|
|
|
|
background-color: #555;
|
|
|
|
|
}}
|
|
|
|
|
QPushButton:pressed {{
|
|
|
|
|
background-color: #222;
|
|
|
|
|
border: 1px solid #888;
|
|
|
|
|
}}
|
|
|
|
|
""")
|
|
|
|
|
return btn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeckWidget(QWidget):
|
2026-04-30 00:30:23 +10:00
|
|
|
track_started = Signal(str)
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
def __init__(self, deck_key: str, label: str, accent_color: str,
|
|
|
|
|
engine, parent=None):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self.deck_key = deck_key
|
|
|
|
|
self.engine = engine
|
|
|
|
|
self.accent_color = accent_color
|
2026-04-30 00:30:23 +10:00
|
|
|
self.current_theme = None
|
2026-05-08 17:19:11 +10:00
|
|
|
self.flash_state = False
|
2026-05-08 18:33:01 +10:00
|
|
|
self._playing_icon = False
|
|
|
|
|
self._playing_icon = False
|
2026-05-08 17:19:11 +10:00
|
|
|
|
|
|
|
|
# Convert "DECK 1" to "DECK_001"
|
|
|
|
|
deck_num = label.split()[-1]
|
|
|
|
|
self.deck_label = f"DECK_{int(deck_num):03d}"
|
2026-04-29 16:21:44 +10:00
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
self._build_ui()
|
2026-04-29 16:21:44 +10:00
|
|
|
self._connect_signals()
|
|
|
|
|
|
|
|
|
|
self.timer = QTimer(self)
|
|
|
|
|
self.timer.setInterval(100)
|
|
|
|
|
self.timer.timeout.connect(self._refresh_display)
|
|
|
|
|
self.timer.start()
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
self.flash_timer = QTimer(self)
|
|
|
|
|
self.flash_timer.setInterval(500)
|
|
|
|
|
self.flash_timer.timeout.connect(self._toggle_flash)
|
|
|
|
|
self.flash_timer.start()
|
|
|
|
|
|
|
|
|
|
def _build_ui(self):
|
2026-04-29 16:21:44 +10:00
|
|
|
self.setStyleSheet(f"""
|
|
|
|
|
QWidget {{
|
|
|
|
|
background-color: #1c1c1c;
|
|
|
|
|
border: 2px solid {self.accent_color};
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
}}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
root = QVBoxLayout(self)
|
|
|
|
|
root.setContentsMargins(10, 10, 10, 10)
|
|
|
|
|
root.setSpacing(8)
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
self.header = QLabel(self.deck_label)
|
|
|
|
|
self.header.setFont(QFont("Courier New", 24, QFont.Bold))
|
|
|
|
|
self.header.setStyleSheet(f"""
|
2026-04-29 16:21:44 +10:00
|
|
|
color: {self.accent_color};
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
2026-05-08 17:19:11 +10:00
|
|
|
padding: 4px;
|
2026-04-29 16:21:44 +10:00
|
|
|
""")
|
2026-05-08 17:19:11 +10:00
|
|
|
self.header.setAlignment(Qt.AlignCenter)
|
|
|
|
|
root.addWidget(self.header)
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
self.lcd = LCDDisplay()
|
|
|
|
|
root.addWidget(self.lcd)
|
|
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
self.waveform = WaveformWidget()
|
|
|
|
|
root.addWidget(self.waveform)
|
|
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
transport = QHBoxLayout()
|
|
|
|
|
transport.setSpacing(6)
|
|
|
|
|
|
2026-05-08 18:33:01 +10:00
|
|
|
self.btn_to_start = make_icon_button(icon_skip_start())
|
|
|
|
|
self.btn_back_15 = make_icon_button(icon_skip_back(), icon_color="#aaaaaa")
|
|
|
|
|
self.btn_play = make_icon_button(icon_play(), "#1a6b3a", "#00ff41", 68, 48)
|
|
|
|
|
self.btn_fwd_15 = make_icon_button(icon_skip_forward(), icon_color="#aaaaaa")
|
|
|
|
|
self.btn_to_end = make_icon_button(icon_skip_end())
|
|
|
|
|
self.btn_stop = make_icon_button(icon_stop(), "#8a1a1a", "#ff4444")
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
transport.addWidget(self.btn_to_start)
|
|
|
|
|
transport.addWidget(self.btn_back_15)
|
|
|
|
|
transport.addWidget(self.btn_play)
|
|
|
|
|
transport.addWidget(self.btn_fwd_15)
|
|
|
|
|
transport.addWidget(self.btn_to_end)
|
|
|
|
|
transport.addStretch()
|
|
|
|
|
transport.addWidget(self.btn_stop)
|
|
|
|
|
|
|
|
|
|
root.addLayout(transport)
|
|
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
def apply_theme(self, theme: dict):
|
|
|
|
|
self.current_theme = theme
|
2026-05-08 17:19:11 +10:00
|
|
|
|
|
|
|
|
border_color = theme['border']
|
|
|
|
|
bg_color = theme['bg2']
|
|
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
self.setStyleSheet(f"""
|
|
|
|
|
QWidget {{
|
2026-05-08 17:19:11 +10:00
|
|
|
background-color: {bg_color};
|
|
|
|
|
border: 2px solid {border_color};
|
2026-04-30 00:30:23 +10:00
|
|
|
border-radius: 6px;
|
|
|
|
|
}}
|
|
|
|
|
""")
|
2026-05-08 17:19:11 +10:00
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
self.lcd.setStyleSheet(f"""
|
|
|
|
|
QFrame {{
|
|
|
|
|
background-color: {theme['bg']};
|
2026-05-08 17:19:11 +10:00
|
|
|
border: 2px solid {border_color};
|
2026-04-30 00:30:23 +10:00
|
|
|
border-radius: 4px;
|
|
|
|
|
}}
|
|
|
|
|
""")
|
2026-05-08 17:19:11 +10:00
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
self.waveform.setStyleSheet(f"""
|
|
|
|
|
QFrame {{
|
|
|
|
|
background-color: {theme['bg']};
|
2026-05-08 17:19:11 +10:00
|
|
|
border: 1px solid {border_color};
|
2026-04-30 00:30:23 +10:00
|
|
|
border-radius: 3px;
|
|
|
|
|
}}
|
|
|
|
|
""")
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
def _toggle_flash(self):
|
|
|
|
|
self.flash_state = not self.flash_state
|
|
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
def _connect_signals(self):
|
|
|
|
|
self.btn_play.clicked.connect(self._toggle_play)
|
|
|
|
|
self.btn_stop.clicked.connect(self._stop_eject)
|
|
|
|
|
self.btn_to_start.clicked.connect(self._go_to_start)
|
|
|
|
|
self.btn_to_end.clicked.connect(self._go_to_end)
|
|
|
|
|
self.btn_back_15.clicked.connect(lambda: self._scrub(-15))
|
|
|
|
|
self.btn_fwd_15.clicked.connect(lambda: self._scrub(15))
|
|
|
|
|
|
|
|
|
|
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")
|
2026-05-08 17:19:11 +10:00
|
|
|
QTimer.singleShot(200, self._update_waveform)
|
|
|
|
|
|
|
|
|
|
def _update_waveform(self):
|
2026-04-30 00:30:23 +10:00
|
|
|
deck = self.engine.decks[self.deck_key]
|
|
|
|
|
if deck.track:
|
|
|
|
|
self.waveform.set_waveform(deck.track.audio)
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
def set_queue_name(self, name: str):
|
|
|
|
|
self.lcd.update_queue(name)
|
|
|
|
|
|
|
|
|
|
def toggle_play(self):
|
|
|
|
|
self._toggle_play()
|
|
|
|
|
|
|
|
|
|
def cue(self):
|
|
|
|
|
deck = self.engine.decks[self.deck_key]
|
|
|
|
|
if deck.track is None:
|
|
|
|
|
return
|
|
|
|
|
if deck.playing:
|
|
|
|
|
deck.pause()
|
|
|
|
|
deck.go_to_start()
|
2026-05-08 18:33:01 +10:00
|
|
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
|
|
|
|
self._playing_icon = False
|
2026-04-29 16:21:44 +10:00
|
|
|
self.btn_play.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #1a6b3a;
|
|
|
|
|
border: 1px solid #555;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #2a7b4a; }
|
|
|
|
|
""")
|
|
|
|
|
self.lcd.update_status("CUE")
|
|
|
|
|
self.lcd.set_playing(False)
|
|
|
|
|
|
|
|
|
|
def scrub(self, delta: float):
|
|
|
|
|
self._scrub(delta)
|
|
|
|
|
|
|
|
|
|
def _toggle_play(self):
|
|
|
|
|
deck = self.engine.decks[self.deck_key]
|
|
|
|
|
if deck.track is None:
|
|
|
|
|
return
|
|
|
|
|
if deck.playing:
|
|
|
|
|
deck.pause()
|
2026-05-08 18:33:01 +10:00
|
|
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
|
|
|
|
self._playing_icon = False
|
2026-04-29 16:21:44 +10:00
|
|
|
self.btn_play.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #1a6b3a;
|
|
|
|
|
border: 1px solid #555;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #2a7b4a; }
|
|
|
|
|
""")
|
|
|
|
|
self.lcd.update_status("PAUSED")
|
|
|
|
|
self.lcd.set_playing(False)
|
|
|
|
|
else:
|
|
|
|
|
deck.play()
|
2026-05-08 18:33:01 +10:00
|
|
|
self.btn_play.setIcon(icon_pause("#ffffff"))
|
|
|
|
|
self._playing_icon = True
|
2026-04-29 16:21:44 +10:00
|
|
|
self.btn_play.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #00aa33;
|
|
|
|
|
border: 1px solid #00ff41;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #00cc44; }
|
|
|
|
|
""")
|
|
|
|
|
self.lcd.update_status("PLAYING")
|
|
|
|
|
self.lcd.set_playing(True)
|
|
|
|
|
if deck.track:
|
|
|
|
|
self.track_started.emit(deck.track.filepath)
|
|
|
|
|
|
|
|
|
|
def _stop_eject(self):
|
|
|
|
|
deck = self.engine.decks[self.deck_key]
|
|
|
|
|
deck.stop()
|
2026-05-08 18:33:01 +10:00
|
|
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
|
|
|
|
self._playing_icon = False
|
2026-04-29 16:21:44 +10:00
|
|
|
self.btn_play.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #1a6b3a;
|
|
|
|
|
border: 1px solid #555;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #2a7b4a; }
|
|
|
|
|
""")
|
|
|
|
|
self.lcd.update_track("NO TRACK LOADED")
|
|
|
|
|
self.lcd.update_time("00:00", "-00:00")
|
|
|
|
|
self.lcd.update_status("STOPPED")
|
|
|
|
|
self.lcd.set_playing(False)
|
2026-04-30 00:30:23 +10:00
|
|
|
self.waveform.set_waveform(None)
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
def _go_to_start(self):
|
|
|
|
|
self.engine.decks[self.deck_key].go_to_start()
|
|
|
|
|
|
|
|
|
|
def _go_to_end(self):
|
|
|
|
|
self.engine.decks[self.deck_key].go_to_end()
|
|
|
|
|
|
|
|
|
|
def _scrub(self, seconds: float):
|
|
|
|
|
self.engine.decks[self.deck_key].skip_seconds(seconds)
|
|
|
|
|
|
|
|
|
|
def _refresh_display(self):
|
|
|
|
|
deck = self.engine.decks[self.deck_key]
|
|
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
# 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])
|
2026-05-08 17:19:11 +10:00
|
|
|
QTimer.singleShot(200, self._update_waveform)
|
2026-04-30 00:30:23 +10:00
|
|
|
deck.queued = None
|
|
|
|
|
self.lcd.update_queue("")
|
|
|
|
|
self.lcd.update_status("LOADED")
|
|
|
|
|
|
|
|
|
|
# Sync button if stopped manually
|
2026-05-08 18:33:01 +10:00
|
|
|
if not deck.playing and self._playing_icon:
|
|
|
|
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
|
|
|
|
self._playing_icon = False
|
2026-04-29 16:21:44 +10:00
|
|
|
self.btn_play.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #1a6b3a;
|
|
|
|
|
border: 1px solid #555;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #2a7b4a; }
|
|
|
|
|
""")
|
|
|
|
|
self.lcd.update_status("STOPPED")
|
|
|
|
|
self.lcd.set_playing(False)
|
|
|
|
|
|
|
|
|
|
if deck.track:
|
|
|
|
|
self.lcd.update_time(
|
|
|
|
|
deck.get_elapsed(),
|
|
|
|
|
deck.get_remaining()
|
|
|
|
|
)
|
2026-05-08 17:19:11 +10:00
|
|
|
|
2026-04-30 00:30:23 +10:00
|
|
|
position_pct = deck.position / deck.track.num_samples
|
2026-05-08 17:19:11 +10:00
|
|
|
remaining_sec = deck.track.duration - (deck.position / deck.track.sr)
|
|
|
|
|
|
|
|
|
|
# Warning if < 20 seconds OR < 15% remaining
|
|
|
|
|
in_warning = (remaining_sec < 20) or (position_pct > 0.85)
|
|
|
|
|
|
|
|
|
|
# Flash header AND all numbers if in warning zone and playing
|
|
|
|
|
if deck.playing and in_warning:
|
|
|
|
|
flash_color = '#ff4444' if self.flash_state else self.accent_color
|
|
|
|
|
|
|
|
|
|
# Flash header
|
|
|
|
|
self.header.setStyleSheet(f"""
|
|
|
|
|
color: {flash_color};
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 4px;
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# Flash LCD numbers
|
|
|
|
|
self.lcd.flash_warning(self.flash_state)
|
|
|
|
|
else:
|
|
|
|
|
# Normal colors
|
|
|
|
|
self.header.setStyleSheet(f"""
|
|
|
|
|
color: {self.accent_color};
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 4px;
|
|
|
|
|
""")
|
|
|
|
|
self.lcd.flash_warning(False)
|
|
|
|
|
|
|
|
|
|
self.waveform.set_position(position_pct, in_warning and self.flash_state)
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
if deck.queued:
|
|
|
|
|
self.lcd.update_queue(
|
|
|
|
|
os.path.basename(deck.queued.filepath)
|
|
|
|
|
)
|