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:
+223
-47
@@ -1,23 +1,21 @@
|
||||
# playlist_widget.py
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QPushButton, QFileDialog, QFrame, QSizePolicy,
|
||||
QListWidget, QListWidgetItem, QAbstractItemView,
|
||||
QMenu, QSplitter
|
||||
QMenu, QSplitter, QTextEdit, QLineEdit
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer, QSize
|
||||
from PySide6.QtCore import Qt, QTimer, QSize, Signal
|
||||
from PySide6.QtGui import QFont, QColor, QAction
|
||||
import json
|
||||
import os
|
||||
|
||||
from icons import icon_save, icon_load
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# PLAYLIST ITEM
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class PlaylistItem(QListWidgetItem):
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
@@ -29,10 +27,6 @@ class PlaylistItem(QListWidgetItem):
|
||||
self.setToolTip(filepath)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# HOUR DISPLAY + CLOCK
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class HourDisplay(QFrame):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -78,10 +72,6 @@ class HourDisplay(QFrame):
|
||||
self.hour_label.setText(f"HOUR {now.strftime('%H:00')}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# DECK SELECTOR
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class DeckSelector(QFrame):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -152,6 +142,11 @@ class DeckSelector(QFrame):
|
||||
return self._selected
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# SHOW LOG PANEL
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# BASE PLAYLIST PANEL
|
||||
# ─────────────────────────────────────────
|
||||
@@ -275,7 +270,131 @@ class BasePlaylistPanel(QWidget):
|
||||
paths.append(item.filepath)
|
||||
return paths
|
||||
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# PREVIEW BAR
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class PreviewBar(QFrame):
|
||||
def __init__(self, engine, parent=None):
|
||||
super().__init__(parent)
|
||||
self.engine = engine
|
||||
self.setFixedHeight(40)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #0a0a12;
|
||||
border: 1px solid #222;
|
||||
border-radius: 3px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(6, 2, 6, 2)
|
||||
layout.setSpacing(6)
|
||||
|
||||
self.track_label = QLabel("NO TRACK")
|
||||
self.track_label.setFont(QFont("Courier New", 9, QFont.Bold))
|
||||
self.track_label.setStyleSheet("color: #4488ff; background: transparent; border: none;")
|
||||
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
layout.addWidget(self.track_label, stretch=1)
|
||||
|
||||
self.time_label = QLabel("--:--")
|
||||
self.time_label.setFont(QFont("Courier New", 12, QFont.Bold))
|
||||
self.time_label.setStyleSheet("color: #4488ff; background: transparent; border: none;")
|
||||
layout.addWidget(self.time_label)
|
||||
|
||||
self.btn_play = QPushButton("\u25B6")
|
||||
self.btn_play.setFixedSize(30, 26)
|
||||
self.btn_play.setFont(QFont("Arial", 10, QFont.Bold))
|
||||
self.btn_play.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #1a3a5a; color: #4488ff;
|
||||
border: 1px solid #4488ff; border-radius: 3px;
|
||||
}
|
||||
QPushButton:hover { background-color: #2a4a6a; }
|
||||
QPushButton:checked { background-color: #1a5a3a; color: #00ff41; border-color: #00ff41; }
|
||||
""")
|
||||
self.btn_play.setCheckable(True)
|
||||
layout.addWidget(self.btn_play)
|
||||
|
||||
self.btn_forward = QPushButton("+10s")
|
||||
self.btn_forward.setFixedSize(40, 26)
|
||||
self.btn_forward.setFont(QFont("Arial", 8, QFont.Bold))
|
||||
self.btn_forward.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2a1a0a; color: #aa6622;
|
||||
border: 1px solid #553311; border-radius: 3px;
|
||||
}
|
||||
QPushButton:hover { background-color: #3a2a1a; }
|
||||
""")
|
||||
layout.addWidget(self.btn_forward)
|
||||
|
||||
self.btn_play.clicked.connect(self._toggle_preview)
|
||||
self.btn_forward.clicked.connect(self._skip_forward)
|
||||
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(100)
|
||||
self.timer.timeout.connect(self._refresh)
|
||||
self._playing = False
|
||||
self._pending_play = False
|
||||
|
||||
def set_playlist(self, playlist):
|
||||
self._playlist = playlist
|
||||
|
||||
def _selected_item(self):
|
||||
item = self._playlist.currentItem()
|
||||
if isinstance(item, PlaylistItem):
|
||||
return item
|
||||
return None
|
||||
|
||||
def _toggle_preview(self):
|
||||
item = self._selected_item()
|
||||
if item is None:
|
||||
self.btn_play.setChecked(False)
|
||||
return
|
||||
|
||||
deck = self.engine.decks['preview']
|
||||
if self._playing:
|
||||
deck.stop()
|
||||
self._playing = False
|
||||
self._pending_play = False
|
||||
self.btn_play.setChecked(False)
|
||||
self.track_label.setText("NO TRACK")
|
||||
self.time_label.setText("--:--")
|
||||
else:
|
||||
self.engine.load_track('preview', item.filepath)
|
||||
self.track_label.setText(item.text()[:30])
|
||||
self.time_label.setText("00:00")
|
||||
self._pending_play = True
|
||||
|
||||
def _skip_forward(self):
|
||||
if not self._playing:
|
||||
return
|
||||
self.engine.decks['preview'].skip_seconds(10)
|
||||
|
||||
def _refresh(self):
|
||||
deck = self.engine.decks['preview']
|
||||
with deck.lock:
|
||||
playing = deck.playing
|
||||
track = deck.track
|
||||
pos = deck.position
|
||||
|
||||
if self._pending_play and track is not None:
|
||||
self._pending_play = False
|
||||
deck.play()
|
||||
self._playing = True
|
||||
|
||||
if playing and track:
|
||||
s = pos / track.sr
|
||||
self.time_label.setText(f"{int(s//60):02d}:{int(s%60):02d}")
|
||||
if pos >= track.num_samples - 1:
|
||||
self._playing = False
|
||||
self.btn_play.setChecked(False)
|
||||
self.track_label.setText("NO TRACK")
|
||||
self.time_label.setText("--:--")
|
||||
elif not playing and self._playing:
|
||||
self._playing = False
|
||||
self.btn_play.setChecked(False)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
@@ -310,15 +429,35 @@ class TrackPlaylistPanel(BasePlaylistPanel):
|
||||
header.setAlignment(Qt.AlignCenter)
|
||||
root.addWidget(header)
|
||||
|
||||
self.preview_bar = PreviewBar(self.engine)
|
||||
root.addWidget(self.preview_bar)
|
||||
|
||||
self.hour_display = HourDisplay()
|
||||
root.addWidget(self.hour_display)
|
||||
|
||||
self.deck_selector = DeckSelector()
|
||||
root.addWidget(self.deck_selector)
|
||||
|
||||
self.search_box = QLineEdit()
|
||||
self.search_box.setPlaceholderText("search...")
|
||||
self.search_box.setFont(QFont("Courier New", 9))
|
||||
self.search_box.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background-color: #0d0d0d;
|
||||
color: #00ff41;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
QLineEdit:focus { border-color: #00ff41; }
|
||||
""")
|
||||
self.search_box.textChanged.connect(self._filter_list)
|
||||
root.addWidget(self.search_box)
|
||||
|
||||
self.playlist = self._make_list()
|
||||
self.playlist.itemDoubleClicked.connect(self._send_to_deck)
|
||||
self.playlist.customContextMenuRequested.connect(self._context_menu)
|
||||
self.preview_bar.set_playlist(self.playlist)
|
||||
root.addWidget(self.playlist)
|
||||
|
||||
row1 = QHBoxLayout()
|
||||
@@ -364,6 +503,13 @@ class TrackPlaylistPanel(BasePlaylistPanel):
|
||||
self.btn_remove.clicked.connect(self._remove_selected)
|
||||
self.btn_clear.clicked.connect(self._clear_all)
|
||||
|
||||
def _filter_list(self):
|
||||
query = self.search_box.text().lower()
|
||||
for i in range(self.playlist.count()):
|
||||
item = self.playlist.item(i)
|
||||
if isinstance(item, PlaylistItem):
|
||||
item.setHidden(bool(query and query not in item.filename.lower()))
|
||||
|
||||
def _send_to_deck(self, item=None):
|
||||
if item is None:
|
||||
item = self.playlist.currentItem()
|
||||
@@ -456,10 +602,10 @@ class TrackPlaylistPanel(BasePlaylistPanel):
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
class CartPlaylistPanel(BasePlaylistPanel):
|
||||
def __init__(self, engine, cart_slots: list, parent=None):
|
||||
def __init__(self, engine, cart_player, parent=None):
|
||||
super().__init__(parent)
|
||||
self.engine = engine
|
||||
self.cart_slots = cart_slots
|
||||
self.engine = engine
|
||||
self.cart_player = cart_player
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self):
|
||||
@@ -483,8 +629,24 @@ class CartPlaylistPanel(BasePlaylistPanel):
|
||||
header.setAlignment(Qt.AlignCenter)
|
||||
root.addWidget(header)
|
||||
|
||||
self.search_box = QLineEdit()
|
||||
self.search_box.setPlaceholderText("search...")
|
||||
self.search_box.setFont(QFont("Courier New", 9))
|
||||
self.search_box.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background-color: #0d0d0d;
|
||||
color: #ffaa00;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
QLineEdit:focus { border-color: #ffaa00; }
|
||||
""")
|
||||
self.search_box.textChanged.connect(self._filter_list)
|
||||
root.addWidget(self.search_box)
|
||||
|
||||
self.playlist = self._make_list()
|
||||
self.playlist.itemDoubleClicked.connect(self._send_to_next_free_cart)
|
||||
self.playlist.itemDoubleClicked.connect(self._send_to_cart)
|
||||
self.playlist.customContextMenuRequested.connect(self._context_menu)
|
||||
root.addWidget(self.playlist)
|
||||
|
||||
@@ -498,11 +660,19 @@ class CartPlaylistPanel(BasePlaylistPanel):
|
||||
|
||||
row2 = QHBoxLayout()
|
||||
row2.setSpacing(4)
|
||||
self.btn_send_cart = self._make_btn("→ LOAD CART", "#3a5a1a")
|
||||
self.btn_send_queue = self._make_btn("→ QUEUE", "#4a3a00")
|
||||
row2.addWidget(self.btn_send_cart)
|
||||
row2.addWidget(self.btn_send_queue)
|
||||
root.addLayout(row2)
|
||||
|
||||
row3 = QHBoxLayout()
|
||||
row3.setSpacing(4)
|
||||
self.btn_remove = self._make_btn("REMOVE", "#5a1a1a")
|
||||
self.btn_clear = self._make_btn("CLEAR ALL", "#3a1a1a")
|
||||
row2.addWidget(self.btn_remove)
|
||||
row2.addWidget(self.btn_clear)
|
||||
root.addLayout(row2)
|
||||
row3.addWidget(self.btn_remove)
|
||||
row3.addWidget(self.btn_clear)
|
||||
root.addLayout(row3)
|
||||
|
||||
self.count_label = QLabel("0 tracks")
|
||||
self.count_label.setFont(QFont("Courier New", 9))
|
||||
@@ -518,27 +688,33 @@ class CartPlaylistPanel(BasePlaylistPanel):
|
||||
self.btn_add_folder.clicked.connect(
|
||||
lambda: self._add_folder_to(self.playlist)
|
||||
)
|
||||
self.btn_send_cart.clicked.connect(self._send_to_cart)
|
||||
self.btn_send_queue.clicked.connect(self._send_to_cart_queue)
|
||||
self.btn_remove.clicked.connect(self._remove_selected)
|
||||
self.btn_clear.clicked.connect(self._clear_all)
|
||||
|
||||
def _send_to_cart(self, item: PlaylistItem, cart_index: int):
|
||||
if not isinstance(item, PlaylistItem):
|
||||
return
|
||||
slot = self.cart_slots[cart_index]
|
||||
slot.load_track(item.filepath)
|
||||
self._highlight(item, active=True)
|
||||
def _filter_list(self):
|
||||
query = self.search_box.text().lower()
|
||||
for i in range(self.playlist.count()):
|
||||
item = self.playlist.item(i)
|
||||
if isinstance(item, PlaylistItem):
|
||||
item.setHidden(bool(query and query not in item.filename.lower()))
|
||||
|
||||
def _send_to_next_free_cart(self, item=None):
|
||||
def _send_to_cart(self, item=None):
|
||||
if item is None:
|
||||
item = self.playlist.currentItem()
|
||||
if not isinstance(item, PlaylistItem):
|
||||
return
|
||||
for i in range(4):
|
||||
cart_key = f"cart{i + 1}"
|
||||
if self.engine.decks[cart_key].track is None:
|
||||
self._send_to_cart(item, i)
|
||||
return
|
||||
self._send_to_cart(item, 0)
|
||||
self.cart_player.load_track(item.filepath)
|
||||
self._highlight(item, active=True)
|
||||
|
||||
def _send_to_cart_queue(self, item=None):
|
||||
if item is None:
|
||||
item = self.playlist.currentItem()
|
||||
if not isinstance(item, PlaylistItem):
|
||||
return
|
||||
self.engine.add_to_cart_queue(item.filepath)
|
||||
self._highlight(item, queued=True)
|
||||
|
||||
def _remove_selected(self):
|
||||
row = self.playlist.currentRow()
|
||||
@@ -565,12 +741,13 @@ class CartPlaylistPanel(BasePlaylistPanel):
|
||||
QMenu::item:selected { background-color: #1a3a5a; }
|
||||
""")
|
||||
|
||||
for i in range(4):
|
||||
action = QAction(f"Load → Cart {i + 1}", self)
|
||||
action.triggered.connect(
|
||||
lambda checked, idx=i: self._send_to_cart(item, idx)
|
||||
)
|
||||
menu.addAction(action)
|
||||
a1 = QAction("Load → Cart", self)
|
||||
a1.triggered.connect(lambda: self._send_to_cart(item))
|
||||
menu.addAction(a1)
|
||||
|
||||
a2 = QAction("Queue → Cart", self)
|
||||
a2.triggered.connect(lambda: self._send_to_cart_queue(item))
|
||||
menu.addAction(a2)
|
||||
|
||||
menu.addSeparator()
|
||||
ar = QAction("Remove", self)
|
||||
@@ -590,7 +767,7 @@ class PlaylistWidget(QWidget):
|
||||
PLAYLIST_JSON_FILTER = "JSON Files (*.json)"
|
||||
|
||||
def __init__(self, engine, deck_widgets: dict,
|
||||
cart_slots: list, parent=None):
|
||||
cart_player, parent=None):
|
||||
super().__init__(parent)
|
||||
self.engine = engine
|
||||
|
||||
@@ -601,7 +778,6 @@ class PlaylistWidget(QWidget):
|
||||
root.setContentsMargins(0, 0, 0, 0)
|
||||
root.setSpacing(4)
|
||||
|
||||
# Toolbar
|
||||
bar = QHBoxLayout()
|
||||
bar.setSpacing(4)
|
||||
|
||||
@@ -653,11 +829,11 @@ class PlaylistWidget(QWidget):
|
||||
""")
|
||||
|
||||
self.track_panel = TrackPlaylistPanel(engine, deck_widgets)
|
||||
self.cart_panel = CartPlaylistPanel(engine, cart_slots)
|
||||
self.cart_panel = CartPlaylistPanel(engine, cart_player)
|
||||
|
||||
splitter.addWidget(self.track_panel)
|
||||
splitter.addWidget(self.cart_panel)
|
||||
splitter.setSizes([600, 400])
|
||||
splitter.setSizes([400, 400])
|
||||
|
||||
root.addWidget(splitter)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user