- Remove TURN decks (3/4), clean up MIDI, layout - Single cart player with 3-item autoload queue + continue toggle - Event logging (SessionLogger), mic tracking, large mic timer widget - LUFS metering moved from external ports to internal master mix - Master output WAV recording with top-bar REC toggle - Music mix output (ch 3-8 only, no mics) - Preview bar for playlist auditioning - Merged show log with [SONG][CART][TALK] prefixes - Live CSV writer for OBS song display (~/live_songs.csv) - Misc fixes: preview race condition, datetime import for recording
364 lines
13 KiB
Python
364 lines
13 KiB
Python
# cart_widget.py
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
QPushButton, QFrame, QSizePolicy, QCheckBox, QScrollArea
|
|
)
|
|
from PySide6.QtCore import Qt, QTimer, QSize, Signal
|
|
from PySide6.QtGui import QFont
|
|
import os
|
|
|
|
from icons import icon_play, icon_pause, icon_eject, icon_delete
|
|
|
|
|
|
class CartPlayerWidget(QFrame):
|
|
"""Single active cart player with visible 3-item autoload queue."""
|
|
cart_triggered = Signal(str)
|
|
|
|
def __init__(self, engine, parent=None):
|
|
super().__init__(parent)
|
|
self.engine = engine
|
|
self._playing_icon = False
|
|
|
|
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
self.setFixedHeight(160)
|
|
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.queue_timer = QTimer(self)
|
|
self.queue_timer.setInterval(500)
|
|
self.queue_timer.timeout.connect(self._refresh_queue)
|
|
self.queue_timer.start()
|
|
|
|
def _build_ui(self):
|
|
self.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #111;
|
|
border: 2px solid #cc3300;
|
|
border-radius: 6px;
|
|
}
|
|
""")
|
|
root = QHBoxLayout(self)
|
|
root.setContentsMargins(10, 8, 10, 8)
|
|
root.setSpacing(10)
|
|
|
|
# ── Left: Cart player box ──────────────────────────────────
|
|
player_frame = QFrame()
|
|
player_frame.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #1a1a1a;
|
|
border: 1px solid #333;
|
|
border-radius: 5px;
|
|
}
|
|
""")
|
|
player_frame.setFixedWidth(320)
|
|
player_layout = QVBoxLayout(player_frame)
|
|
player_layout.setContentsMargins(10, 8, 10, 8)
|
|
player_layout.setSpacing(6)
|
|
|
|
header_row = QHBoxLayout()
|
|
badge = QLabel("1")
|
|
badge.setFixedSize(26, 26)
|
|
badge.setAlignment(Qt.AlignCenter)
|
|
badge.setFont(QFont("Arial", 12, QFont.Bold))
|
|
badge.setStyleSheet("""
|
|
background-color: #cc3300;
|
|
color: white;
|
|
border-radius: 15px;
|
|
border: none;
|
|
""")
|
|
header_row.addWidget(badge)
|
|
|
|
cart_label = QLabel("CART")
|
|
cart_label.setFont(QFont("Arial", 12, QFont.Bold))
|
|
cart_label.setStyleSheet("color: #cc3300; background: transparent; border: none;")
|
|
header_row.addWidget(cart_label)
|
|
|
|
self.continue_toggle = QCheckBox("CONTINUE")
|
|
self.continue_toggle.setFont(QFont("Arial", 9, QFont.Bold))
|
|
self.continue_toggle.setStyleSheet("""
|
|
QCheckBox {
|
|
color: #888; background: transparent; border: none; spacing: 4px;
|
|
}
|
|
QCheckBox::indicator {
|
|
width: 16px; height: 16px;
|
|
border: 1px solid #555; border-radius: 3px;
|
|
background-color: #1a1a1a;
|
|
}
|
|
QCheckBox::indicator:checked {
|
|
background-color: #cc3300; border-color: #ff6644;
|
|
}
|
|
QCheckBox::indicator:hover { border-color: #888; }
|
|
""")
|
|
self.continue_toggle.setToolTip("Auto-play next track from queue after current finishes")
|
|
self.continue_toggle.stateChanged.connect(self._on_continue_toggled)
|
|
header_row.addWidget(self.continue_toggle)
|
|
header_row.addStretch()
|
|
player_layout.addLayout(header_row)
|
|
|
|
# LCD display
|
|
self.lcd_frame = QFrame()
|
|
self.lcd_frame.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #0a1a0a;
|
|
border: 1px solid #1a2a1a;
|
|
border-radius: 3px;
|
|
}
|
|
""")
|
|
lcd_layout = QHBoxLayout(self.lcd_frame)
|
|
lcd_layout.setContentsMargins(8, 4, 8, 4)
|
|
lcd_layout.setSpacing(6)
|
|
|
|
lcd_left = QVBoxLayout()
|
|
lcd_left.setSpacing(2)
|
|
|
|
self.track_label = QLabel("EMPTY")
|
|
self.track_label.setFont(QFont("Courier New", 9, QFont.Bold))
|
|
self.track_label.setStyleSheet("color: #00ff41; background: transparent; border: none;")
|
|
self.track_label.setMinimumWidth(180)
|
|
|
|
time_row = QHBoxLayout()
|
|
self.elapsed_label = QLabel("00:00")
|
|
self.elapsed_label.setFont(QFont("Courier New", 18, QFont.Bold))
|
|
self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent; border: none;")
|
|
|
|
self.remaining_label = QLabel("-00:00")
|
|
self.remaining_label.setFont(QFont("Courier New", 10, QFont.Bold))
|
|
self.remaining_label.setStyleSheet("color: #00cc33; background: transparent; border: none;")
|
|
self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
|
|
time_row.addWidget(self.elapsed_label)
|
|
time_row.addStretch()
|
|
time_row.addWidget(self.remaining_label)
|
|
|
|
self.status_label = QLabel("STOPPED")
|
|
self.status_label.setFont(QFont("Courier New", 8))
|
|
self.status_label.setStyleSheet("color: #008822; background: transparent; border: none;")
|
|
|
|
lcd_left.addWidget(self.track_label)
|
|
lcd_left.addLayout(time_row)
|
|
lcd_left.addWidget(self.status_label)
|
|
lcd_layout.addLayout(lcd_left)
|
|
|
|
btn_col = QVBoxLayout()
|
|
btn_col.setSpacing(3)
|
|
|
|
self.btn_play = QPushButton()
|
|
self.btn_play.setFixedSize(56, 36)
|
|
self.btn_play.setIconSize(QSize(22, 22))
|
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
|
self.btn_play.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #1a6b3a;
|
|
border: 1px solid #555;
|
|
border-radius: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #2a7b4a; }
|
|
QPushButton:pressed { background-color: #0a5a2a; }
|
|
""")
|
|
|
|
self.btn_eject = QPushButton()
|
|
self.btn_eject.setFixedSize(56, 28)
|
|
self.btn_eject.setIconSize(QSize(16, 16))
|
|
self.btn_eject.setIcon(icon_eject("#ff4444"))
|
|
self.btn_eject.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #8a1a1a;
|
|
border: 1px solid #555;
|
|
border-radius: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #9a2a2a; }
|
|
QPushButton:pressed { background-color: #6a0a0a; }
|
|
""")
|
|
|
|
btn_col.addWidget(self.btn_play)
|
|
btn_col.addWidget(self.btn_eject)
|
|
lcd_layout.addLayout(btn_col)
|
|
|
|
player_layout.addWidget(self.lcd_frame)
|
|
root.addWidget(player_frame)
|
|
|
|
# ── Right: Queue panel ──────────────────────────────────────
|
|
queue_frame = QFrame()
|
|
queue_frame.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #111;
|
|
border: 1px solid #2a2010;
|
|
border-radius: 5px;
|
|
}
|
|
""")
|
|
queue_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
queue_layout = QVBoxLayout(queue_frame)
|
|
queue_layout.setContentsMargins(8, 6, 8, 6)
|
|
queue_layout.setSpacing(2)
|
|
|
|
queue_header = QHBoxLayout()
|
|
ql = QLabel("AUTOLOAD QUEUE")
|
|
ql.setFont(QFont("Arial", 9, QFont.Bold))
|
|
ql.setStyleSheet("color: #ffaa00; background: transparent; border: none;")
|
|
queue_header.addWidget(ql)
|
|
queue_header.addStretch()
|
|
|
|
self.queue_count = QLabel("0/3")
|
|
self.queue_count.setFont(QFont("Courier New", 9, QFont.Bold))
|
|
self.queue_count.setStyleSheet("color: #555; background: transparent; border: none;")
|
|
queue_header.addWidget(self.queue_count)
|
|
|
|
clear_btn = QPushButton("CLR")
|
|
clear_btn.setFixedSize(40, 20)
|
|
clear_btn.setFont(QFont("Arial", 7, QFont.Bold))
|
|
clear_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3a1a1a;
|
|
color: #ff6666;
|
|
border: 1px solid #555;
|
|
border-radius: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #5a2a2a; }
|
|
""")
|
|
clear_btn.clicked.connect(self.engine.clear_cart_queue)
|
|
queue_header.addWidget(clear_btn)
|
|
queue_layout.addLayout(queue_header)
|
|
|
|
self.queue_items = []
|
|
for i in range(3):
|
|
item = QLabel(f" — ")
|
|
item.setFont(QFont("Courier New", 9))
|
|
item.setStyleSheet("color: #555; background: transparent; border: none; padding: 2px 4px;")
|
|
item.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
queue_layout.addWidget(item)
|
|
self.queue_items.append(item)
|
|
|
|
queue_layout.addStretch()
|
|
root.addWidget(queue_frame)
|
|
|
|
def _connect_signals(self):
|
|
self.btn_play.clicked.connect(self._toggle_play)
|
|
self.btn_eject.clicked.connect(self._eject)
|
|
|
|
def _on_continue_toggled(self, state):
|
|
self.engine.cart_continue = bool(state)
|
|
|
|
def load_track(self, filepath: str):
|
|
self.engine.load_cart_track("cart1", filepath)
|
|
name = os.path.basename(filepath)
|
|
short = name[:32] + "..." if len(name) > 32 else name
|
|
self.track_label.setText(short)
|
|
self.status_label.setText("LOADED")
|
|
self.elapsed_label.setText("00:00")
|
|
self.remaining_label.setText("-00:00")
|
|
|
|
def toggle(self):
|
|
deck = self.engine.decks["cart1"]
|
|
if deck.track is None:
|
|
return
|
|
if deck.playing:
|
|
deck.stop()
|
|
deck.go_to_start()
|
|
self._set_ui_stopped()
|
|
self.status_label.setText("READY")
|
|
else:
|
|
deck.play()
|
|
self._set_ui_playing()
|
|
self.engine.log_cart_trigger(deck.track.filename)
|
|
self.cart_triggered.emit(deck.track.filename)
|
|
|
|
def _set_ui_playing(self):
|
|
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: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #00cc44; }
|
|
""")
|
|
self.status_label.setText("PLAYING")
|
|
self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent; border: none;")
|
|
|
|
def _set_ui_stopped(self):
|
|
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: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #2a7b4a; }
|
|
""")
|
|
self.elapsed_label.setText("00:00")
|
|
self.remaining_label.setText("-00:00")
|
|
self.elapsed_label.setStyleSheet("color: #00aa2a; background: transparent; border: none;")
|
|
|
|
def _toggle_play(self):
|
|
deck = self.engine.decks["cart1"]
|
|
if deck.track is None:
|
|
return
|
|
if deck.playing:
|
|
deck.pause()
|
|
self._set_ui_stopped()
|
|
self.status_label.setText("PAUSED")
|
|
else:
|
|
deck.play()
|
|
self._set_ui_playing()
|
|
self.engine.log_cart_trigger(deck.track.filename)
|
|
self.cart_triggered.emit(deck.track.filename)
|
|
|
|
def _eject(self):
|
|
deck = self.engine.decks["cart1"]
|
|
deck.stop()
|
|
self._set_ui_stopped()
|
|
self.track_label.setText("EMPTY")
|
|
self.status_label.setText("STOPPED")
|
|
|
|
def _refresh_display(self):
|
|
deck = self.engine.decks["cart1"]
|
|
track = deck.track
|
|
playing = deck.playing
|
|
|
|
if not playing and self._playing_icon:
|
|
self._set_ui_stopped()
|
|
self.status_label.setText("STOPPED")
|
|
|
|
if track:
|
|
self.elapsed_label.setText(deck.get_elapsed())
|
|
self.remaining_label.setText(deck.get_remaining())
|
|
|
|
def _refresh_queue(self):
|
|
q = self.engine.cart_queue
|
|
qlen = len(q)
|
|
self.queue_count.setText(f"{qlen}/3")
|
|
if qlen == 0:
|
|
self.queue_count.setStyleSheet("color: #555; background: transparent; border: none;")
|
|
elif qlen < 3:
|
|
self.queue_count.setStyleSheet("color: #ffaa00; background: transparent; border: none;")
|
|
else:
|
|
self.queue_count.setStyleSheet("color: #ff4444; background: transparent; border: none;")
|
|
|
|
for i in range(3):
|
|
if i < qlen:
|
|
name = q[i].filename
|
|
name = name[:35] + "..." if len(name) > 35 else name
|
|
self.queue_items[i].setText(f" {i+1}. {name}")
|
|
self.queue_items[i].setStyleSheet("color: #ffaa00; background: transparent; border: none; padding: 2px 4px;")
|
|
else:
|
|
self.queue_items[i].setText(" — ")
|
|
self.queue_items[i].setStyleSheet("color: #333; background: transparent; border: none; padding: 2px 4px;")
|
|
|
|
if not self.engine.cart_continue and self.continue_toggle.isChecked():
|
|
self.continue_toggle.blockSignals(True)
|
|
self.continue_toggle.setChecked(False)
|
|
self.continue_toggle.blockSignals(False)
|
|
|
|
def apply_theme(self, theme: dict):
|
|
self.lcd_frame.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {theme['bg']};
|
|
border: 1px solid {theme['border']};
|
|
border-radius: 3px;
|
|
}}
|
|
""") |