Files
gti_radiostudio/deck_widget.py
T
nickmin 89fb7010fb v0.5 — cleanup orphan code, add MIDI master volume learn, fix turn-deck crashes
- Remove vestigial lufs_meter_widget.py and debug_audio.py
- Delete unused WaveformWidget/WaveformMonitor classes, dead stubs, duplicate code
- Fix turn-mode deck crashes (deck3/deck4 missing from engine.decks, uninitialized manual_elapsed/duration)
- Replace raw print() with logging in main.py
- Add master_volume MIDI learn signal + settings popup entry
- Bump version to 0.5
2026-05-14 22:25:10 +10:00

467 lines
16 KiB
Python

# deck_widget.py
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QSizePolicy, QInputDialog
)
from PySide6.QtCore import Qt, QTimer, Signal, QSize
from PySide6.QtGui import QFont, QColor
import os
from icons import (
icon_play, icon_pause, icon_stop, icon_skip_start,
icon_skip_end, icon_skip_back, icon_skip_forward, icon_eject
)
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_btn = QPushButton("NO TRACK LOADED")
self.track_btn.setFont(QFont("Courier New", 13, QFont.Bold))
self.track_btn.setStyleSheet("""
QPushButton {
color: #00ff41; background: transparent; border: none;
text-align: left; padding: 0px;
}
QPushButton:hover { color: #ffffff; }
""")
self.track_btn.setCursor(Qt.PointingHandCursor)
self.track_btn.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_btn)
layout.addLayout(time_row)
layout.addLayout(status_row)
def update_track(self, name: str):
if len(name) > 40:
name = name[:37] + "..."
self.track_btn.setText(name)
def track_name(self) -> str:
return self.track_btn.text()
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;")
def flash_warning(self, active: bool):
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;")
def make_icon_button(icon, color="#3a3a3a", icon_color="white",
width=52, height=48):
btn = QPushButton()
btn.setFixedSize(width, height)
btn.setIconSize(QSize(22, 22))
btn.setIcon(icon)
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):
track_started = Signal(str)
def __init__(self, deck_key: str, label: str, accent_color: str,
engine, parent=None, mode='deck'):
super().__init__(parent)
self.deck_key = deck_key
self.engine = engine
self.accent_color = accent_color
self.current_theme = None
self.flash_state = False
self._playing_icon = False
self.mode = mode
self.deck_label = label
self._build_ui()
self._connect_signals()
self.timer = QTimer(self)
self.timer.setInterval(100)
self.timer.timeout.connect(self._refresh_display)
self.timer.start()
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):
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)
self.header = QLabel(self.deck_label)
self.header.setFont(QFont("Courier New", 24, QFont.Bold))
self.header.setStyleSheet(f"""
color: {self.accent_color};
background: transparent;
border: none;
padding: 4px;
""")
self.header.setAlignment(Qt.AlignCenter)
root.addWidget(self.header)
self.lcd = LCDDisplay()
root.addWidget(self.lcd)
transport = QHBoxLayout()
transport.setSpacing(6)
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")
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)
if self.mode == 'turn':
self.btn_to_start.setVisible(False)
self.btn_back_15.setVisible(False)
self.btn_fwd_15.setVisible(False)
self.btn_to_end.setVisible(False)
def apply_theme(self, theme: dict):
self.current_theme = theme
border_color = theme['border']
bg_color = theme['bg2']
self.setStyleSheet(f"""
QWidget {{
background-color: {bg_color};
border: 2px solid {border_color};
border-radius: 6px;
}}
""")
self.lcd.setStyleSheet(f"""
QFrame {{
background-color: {theme['bg']};
border: 2px solid {border_color};
border-radius: 4px;
}}
""")
def _toggle_flash(self):
self.flash_state = not self.flash_state
def _connect_signals(self):
self.btn_play.clicked.connect(self._toggle_play)
self.btn_stop.clicked.connect(self._stop_eject)
self.lcd.track_btn.clicked.connect(self._edit_track_name)
if self.mode != 'turn':
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 _edit_track_name(self):
current = self.lcd.track_btn.text()
if current == "NO TRACK LOADED":
current = ""
name, ok = QInputDialog.getText(self, "Track Name", "Track name:", text=current)
if ok:
if not name.strip():
name = "NO TRACK LOADED"
self.lcd.update_track(name.strip())
if self.mode == 'turn':
deck = self.engine.decks[self.deck_key]
if self.mode == 'turn' and name.strip() != "NO TRACK LOADED":
dur, ok2 = QInputDialog.getText(self, "Duration", "Duration (MM:SS):", text="00:00")
if ok2 and ':' in dur:
parts = dur.split(':')
try:
total_sec = int(parts[0]) * 60 + int(parts[1])
deck = self.engine.decks[self.deck_key]
deck.manual_duration = float(total_sec)
except ValueError:
pass
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")
def set_queue_name(self, name: str):
self.lcd.update_queue(name)
def toggle_play(self):
self._toggle_play()
def cue(self):
if self.mode == 'turn':
return
deck = self.engine.decks[self.deck_key]
if deck.track is None:
return
if deck.playing:
deck.pause()
deck.go_to_start()
self.btn_play.setIcon(icon_play("#00ff41"))
self._playing_icon = False
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 and self.mode != 'turn':
return
if deck.playing:
if self.mode == 'turn':
deck.playing = False
else:
deck.pause()
self.btn_play.setIcon(icon_play("#00ff41"))
self._playing_icon = False
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" if self.mode != 'turn' else "STOPPED")
self.lcd.set_playing(False)
else:
if self.mode == 'turn':
deck.playing = True
else:
deck.play()
self.btn_play.setIcon(icon_pause("#ffffff"))
self._playing_icon = True
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 self.mode == 'turn':
deck.manual_elapsed = 0.0
name = self.lcd.track_btn.text()
if name and name != "NO TRACK LOADED":
self.track_started.emit(f"[TURN] {name}")
elif deck.track:
self.track_started.emit(deck.track.filepath)
def _stop_eject(self):
deck = self.engine.decks[self.deck_key]
if self.mode == 'turn':
deck.playing = False
deck.manual_elapsed = 0.0
else:
deck.stop()
self.btn_play.setIcon(icon_play("#00ff41"))
self._playing_icon = False
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)
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]
if self.mode == 'turn':
if deck.playing:
deck.manual_elapsed += 0.1
if deck.manual_duration > 0 and deck.manual_elapsed >= deck.manual_duration:
deck.playing = False
self.btn_play.setIcon(icon_play("#00ff41"))
self._playing_icon = False
self.lcd.update_status("STOPPED")
self.lcd.set_playing(False)
elapsed_s = int(deck.manual_elapsed)
elapsed = f"{elapsed_s // 60:02d}:{elapsed_s % 60:02d}"
if deck.manual_duration > 0:
remaining = max(0, deck.manual_duration - deck.manual_elapsed)
remaining_s = int(remaining)
remaining_str = f"-{remaining_s // 60:02d}:{remaining_s % 60:02d}"
else:
remaining_str = "-00:00"
self.lcd.update_time(elapsed, remaining_str)
return
track = deck.track
playing = deck.playing
position = deck.position
queued = deck.queued
if not playing and track is not None:
if position >= track.num_samples - 1:
if queued:
deck.load(queued)
self.lcd.update_track(os.path.splitext(os.path.basename(queued.filepath))[0])
deck.queued = None
self.lcd.update_queue("")
self.lcd.update_status("LOADED")
if not playing and self._playing_icon:
self.btn_play.setIcon(icon_play("#00ff41"))
self._playing_icon = False
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 track:
self.lcd.update_time(deck.get_elapsed(), deck.get_remaining())
position_pct = position / track.num_samples
remaining_sec = track.duration - (position / track.sr)
in_warning = (remaining_sec < 20) or (position_pct > 0.85)
if playing and in_warning:
flash_color = '#ff4444' if self.flash_state else self.accent_color
self.header.setStyleSheet(f"""
color: {flash_color};
background: transparent;
border: none;
padding: 4px;
""")
self.lcd.flash_warning(self.flash_state)
else:
self.header.setStyleSheet(f"""
color: {self.accent_color};
background: transparent;
border: none;
padding: 4px;
""")
self.lcd.flash_warning(False)
if queued:
self.lcd.update_queue(os.path.basename(queued.filepath))