Overhaul: 5-task implementation

- 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
This commit is contained in:
2026-07-17 20:44:15 +10:00
parent da8240a9e2
commit 0277089b2d
13 changed files with 1015 additions and 326 deletions
+198 -129
View File
@@ -2,35 +2,27 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QSizePolicy, QGridLayout
QPushButton, QFrame, QSizePolicy, QCheckBox, QScrollArea
)
from PySide6.QtCore import Qt, QTimer, QSize
from PySide6.QtCore import Qt, QTimer, QSize, Signal
from PySide6.QtGui import QFont
import os
from icons import icon_play, icon_pause, icon_eject
from icons import icon_play, icon_pause, icon_eject, icon_delete
class CartSlot(QFrame):
def __init__(self, cart_key: str, slot_number: int, engine, parent=None):
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.cart_key = cart_key
self.slot_number = slot_number
self.engine = engine
self.engine = engine
self._playing_icon = False
self.setFrameStyle(QFrame.Box | QFrame.Raised)
self.setLineWidth(1)
self.setStyleSheet("""
QFrame {
background-color: #1e1e1e;
border: 1px solid #444;
border-radius: 5px;
}
""")
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setFixedHeight(80)
self.setFixedHeight(160)
self._build_ui()
self._connect_signals()
@@ -39,23 +31,78 @@ class CartSlot(QFrame):
self.timer.timeout.connect(self._refresh_display)
self.timer.start()
def _build_ui(self):
root = QHBoxLayout(self)
root.setContentsMargins(6, 4, 6, 4)
root.setSpacing(6)
self.queue_timer = QTimer(self)
self.queue_timer.setInterval(500)
self.queue_timer.timeout.connect(self._refresh_queue)
self.queue_timer.start()
badge = QLabel(str(self.slot_number))
badge.setFixedSize(22, 22)
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", 10, QFont.Bold))
badge.setFont(QFont("Arial", 12, QFont.Bold))
badge.setStyleSheet("""
background-color: #cc3300;
color: white;
border-radius: 14px;
border-radius: 15px;
border: none;
""")
root.addWidget(badge, alignment=Qt.AlignVCenter)
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 {
@@ -64,32 +111,26 @@ class CartSlot(QFrame):
border-radius: 3px;
}
""")
self.lcd_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
lcd_layout = QHBoxLayout(self.lcd_frame)
lcd_layout.setContentsMargins(8, 4, 8, 4)
lcd_layout.setSpacing(6)
lcd_layout = QVBoxLayout(self.lcd_frame)
lcd_layout.setContentsMargins(4, 2, 4, 2)
lcd_layout.setSpacing(1)
lcd_left = QVBoxLayout()
lcd_left.setSpacing(2)
self.track_label = QLabel("EMPTY")
self.track_label.setFont(QFont("Courier New", 8, QFont.Bold))
self.track_label.setStyleSheet(
"color: #00ff41; background: transparent; border: none;"
)
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
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", 11, QFont.Bold))
self.elapsed_label.setStyleSheet(
"color: #00ff41; background: transparent; border: none;"
)
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", 8))
self.remaining_label.setStyleSheet(
"color: #00cc33; background: transparent; border: none;"
)
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)
@@ -98,22 +139,19 @@ class CartSlot(QFrame):
self.status_label = QLabel("STOPPED")
self.status_label.setFont(QFont("Courier New", 8))
self.status_label.setStyleSheet(
"color: #008822; background: transparent; border: none;"
)
self.status_label.setStyleSheet("color: #008822; background: transparent; border: none;")
lcd_layout.addWidget(self.track_label)
lcd_layout.addLayout(time_row)
lcd_layout.addWidget(self.status_label)
root.addWidget(self.lcd_frame)
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(50, 32)
self.btn_play.setIconSize(QSize(18, 18))
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 {
@@ -126,8 +164,8 @@ class CartSlot(QFrame):
""")
self.btn_eject = QPushButton()
self.btn_eject.setFixedSize(50, 24)
self.btn_eject.setIconSize(QSize(14, 14))
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 {
@@ -141,36 +179,85 @@ class CartSlot(QFrame):
btn_col.addWidget(self.btn_play)
btn_col.addWidget(self.btn_eject)
lcd_layout.addLayout(btn_col)
root.addLayout(btn_col)
player_layout.addWidget(self.lcd_frame)
root.addWidget(player_frame)
def apply_theme(self, theme: dict):
self.lcd_frame.setStyleSheet(f"""
QFrame {{
background-color: {theme['bg']};
border: 1px solid {theme['border']};
border-radius: 3px;
}}
# ── 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(self.cart_key, filepath)
name = os.path.basename(filepath)
short = name[:28] + "..." if len(name) > 28 else name
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[self.cart_key]
deck = self.engine.decks["cart1"]
if deck.track is None:
return
if deck.playing:
deck.stop()
deck.go_to_start()
@@ -179,42 +266,36 @@ class CartSlot(QFrame):
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;
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;"
)
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;
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;"
)
self.elapsed_label.setStyleSheet("color: #00aa2a; background: transparent; border: none;")
def _toggle_play(self):
deck = self.engine.decks[self.cart_key]
deck = self.engine.decks["cart1"]
if deck.track is None:
return
if deck.playing:
@@ -224,16 +305,18 @@ class CartSlot(QFrame):
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[self.cart_key]
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[self.cart_key]
deck = self.engine.decks["cart1"]
track = deck.track
playing = deck.playing
@@ -245,51 +328,37 @@ class CartSlot(QFrame):
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;")
class CartBankWidget(QWidget):
def __init__(self, engine, parent=None):
super().__init__(parent)
self.engine = engine
self._build_ui()
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;")
def _build_ui(self):
self.setStyleSheet("""
QWidget {
background-color: #111;
border: 2px solid #cc3300;
border-radius: 6px;
}
""")
root = QVBoxLayout(self)
root.setContentsMargins(8, 6, 8, 6)
root.setSpacing(6)
header = QLabel("CARTS")
header.setFont(QFont("Arial", 11, QFont.Bold))
header.setStyleSheet("""
color: #cc3300;
background: transparent;
border: none;
padding: 2px;
""")
header.setAlignment(Qt.AlignCenter)
root.addWidget(header)
grid = QGridLayout()
grid.setSpacing(6)
self.slots = []
for i in range(4):
cart_key = f"cart{i + 1}"
slot = CartSlot(cart_key, i + 1, self.engine)
row = i // 2
col = i % 2
grid.addWidget(slot, row, col)
self.slots.append(slot)
root.addLayout(grid)
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):
for slot in self.slots:
slot.apply_theme(theme)
self.lcd_frame.setStyleSheet(f"""
QFrame {{
background-color: {theme['bg']};
border: 1px solid {theme['border']};
border-radius: 3px;
}}
""")