2026-05-12 19:29:16 +10:00
|
|
|
|
# mixer_widget.py
|
|
|
|
|
|
|
|
|
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSlider,
|
2026-05-15 16:42:10 +10:00
|
|
|
|
QPushButton, QFrame, QScrollArea, QSizePolicy,
|
|
|
|
|
|
QInputDialog, QDial
|
2026-05-12 19:29:16 +10:00
|
|
|
|
)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
from PySide6.QtCore import Qt, QTimer, QSize
|
2026-05-12 19:29:16 +10:00
|
|
|
|
from PySide6.QtGui import QFont, QPainter, QColor, QLinearGradient, QPen
|
|
|
|
|
|
|
|
|
|
|
|
from audio_engine import NUM_MIXER_CHANNELS
|
2026-05-15 08:41:40 +10:00
|
|
|
|
from icons import icon_mute, icon_mic
|
2026-05-13 20:53:15 +10:00
|
|
|
|
import math
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VUMeter(QWidget):
|
2026-05-13 16:47:02 +10:00
|
|
|
|
def __init__(self, parent=None):
|
2026-05-12 19:29:16 +10:00
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self._value = 0.0
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self.setMinimumWidth(28)
|
|
|
|
|
|
self.setMinimumHeight(80)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
def set_value(self, value: float):
|
|
|
|
|
|
self._value = max(0.0, min(1.0, value))
|
|
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
|
|
|
|
def paintEvent(self, event):
|
|
|
|
|
|
painter = QPainter(self)
|
|
|
|
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
|
|
|
2026-05-13 20:53:15 +10:00
|
|
|
|
rect = self.rect().adjusted(18, 3, -3, -3)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
if rect.width() < 1 or rect.height() < 1:
|
|
|
|
|
|
painter.end()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
|
painter.fillRect(rect, QColor("#080808"))
|
|
|
|
|
|
|
2026-05-13 20:53:15 +10:00
|
|
|
|
db_markings = [(-40,), (-30,), (-20,), (-12,), (-6,), (0,)]
|
|
|
|
|
|
for (db,) in db_markings:
|
2026-05-13 16:47:02 +10:00
|
|
|
|
pct = (db + 46) / 46.0 if db <= 0 else 1.0
|
|
|
|
|
|
pct = max(0.0, min(1.0, pct))
|
|
|
|
|
|
y = rect.bottom() - int(rect.height() * pct)
|
2026-05-13 20:53:15 +10:00
|
|
|
|
if db == -6:
|
|
|
|
|
|
painter.setPen(QPen(QColor("#00ff88"), 2))
|
|
|
|
|
|
painter.drawLine(rect.right() - 3, y, rect.right(), y)
|
2026-05-15 08:41:40 +10:00
|
|
|
|
painter.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
2026-05-13 20:53:15 +10:00
|
|
|
|
painter.setPen(QPen(QColor("#ffffff"), 1))
|
|
|
|
|
|
else:
|
|
|
|
|
|
painter.setPen(QPen(QColor("#2a2a2a"), 1))
|
|
|
|
|
|
painter.drawLine(rect.right() - 5, y, rect.right(), y)
|
|
|
|
|
|
painter.setPen(QPen(QColor("#cccccc"), 1))
|
2026-05-15 08:41:40 +10:00
|
|
|
|
painter.setFont(QFont("Segoe UI", 8))
|
2026-05-13 20:53:15 +10:00
|
|
|
|
painter.drawText(rect.left() - 16, y + 3, f"{db}")
|
2026-05-13 16:47:02 +10:00
|
|
|
|
|
|
|
|
|
|
painter.setPen(QPen(QColor("#1a1a1a"), 1))
|
|
|
|
|
|
painter.drawRect(rect)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
val = self._value
|
|
|
|
|
|
if val < 1e-10:
|
|
|
|
|
|
painter.end()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
gradient = QLinearGradient(0, rect.bottom(), 0, rect.top())
|
|
|
|
|
|
gradient.setColorAt(0.0, QColor("#003300"))
|
|
|
|
|
|
gradient.setColorAt(0.5, QColor("#00cc00"))
|
|
|
|
|
|
gradient.setColorAt(0.75, QColor("#cccc00"))
|
|
|
|
|
|
gradient.setColorAt(0.9, QColor("#ff6600"))
|
|
|
|
|
|
gradient.setColorAt(1.0, QColor("#ff0000"))
|
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
|
db = -46 * (1 - val) if val < 1.0 else 3
|
|
|
|
|
|
fill_pct = (db + 46) / 49.0 if db <= 0 else 1.0
|
|
|
|
|
|
fill_pct = max(0.0, min(1.0, fill_pct))
|
2026-05-13 20:53:15 +10:00
|
|
|
|
fill_pct = math.sqrt(fill_pct)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
|
2026-05-12 19:29:16 +10:00
|
|
|
|
fill_h = int(rect.height() * fill_pct)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
painter.fillRect(rect.x(), rect.bottom() - fill_h,
|
|
|
|
|
|
rect.width(), fill_h, gradient)
|
|
|
|
|
|
|
2026-05-13 20:53:15 +10:00
|
|
|
|
for (db,) in db_markings:
|
2026-05-13 16:47:02 +10:00
|
|
|
|
pct = (db + 46) / 46.0 if db <= 0 else 1.0
|
|
|
|
|
|
pct = max(0.0, min(1.0, pct))
|
|
|
|
|
|
y = rect.bottom() - int(rect.height() * pct)
|
|
|
|
|
|
painter.setPen(QPen(QColor("#000000"), 1))
|
|
|
|
|
|
painter.drawLine(rect.x(), y, rect.right(), y)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
painter.end()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChannelStrip(QFrame):
|
|
|
|
|
|
def __init__(self, engine, channel_index, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.engine = engine
|
|
|
|
|
|
self.channel_index = channel_index
|
|
|
|
|
|
self._channel = engine.get_channel(channel_index)
|
|
|
|
|
|
|
2026-05-15 08:41:40 +10:00
|
|
|
|
self.setFixedWidth(100)
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.setStyleSheet("""
|
2026-05-13 16:47:02 +10:00
|
|
|
|
ChannelStrip {
|
|
|
|
|
|
background-color: #181a1e;
|
|
|
|
|
|
border: 1px solid #282a30;
|
|
|
|
|
|
border-radius: 6px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
self._build_ui()
|
|
|
|
|
|
self._connect_signals()
|
|
|
|
|
|
|
|
|
|
|
|
self.timer = QTimer(self)
|
2026-05-13 20:53:15 +10:00
|
|
|
|
self.timer.setInterval(30)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.timer.timeout.connect(self._refresh)
|
|
|
|
|
|
self.timer.start()
|
|
|
|
|
|
|
|
|
|
|
|
def _build_ui(self):
|
|
|
|
|
|
root = QVBoxLayout(self)
|
|
|
|
|
|
root.setContentsMargins(4, 6, 4, 6)
|
|
|
|
|
|
root.setSpacing(4)
|
|
|
|
|
|
|
|
|
|
|
|
self.name_btn = QPushButton(self._channel.name)
|
2026-05-15 08:41:40 +10:00
|
|
|
|
self.name_btn.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.name_btn.setStyleSheet("""
|
|
|
|
|
|
QPushButton {
|
|
|
|
|
|
background-color: transparent;
|
2026-05-13 16:47:02 +10:00
|
|
|
|
color: #999aaa;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 2px;
|
2026-05-15 08:41:40 +10:00
|
|
|
|
font-size: 9pt;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
|
color: #ffffff;
|
2026-05-13 16:47:02 +10:00
|
|
|
|
background-color: #22252c;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
root.addWidget(self.name_btn)
|
|
|
|
|
|
|
2026-05-15 16:42:10 +10:00
|
|
|
|
self.gain_knob = QDial()
|
|
|
|
|
|
self.gain_knob.setRange(0, 100)
|
|
|
|
|
|
self.gain_knob.setValue(50)
|
|
|
|
|
|
self.gain_knob.setFixedSize(40, 40)
|
|
|
|
|
|
self.gain_knob.setNotchesVisible(True)
|
|
|
|
|
|
self.gain_knob.setWrapping(False)
|
|
|
|
|
|
self.gain_knob.setToolTip("Pre-fader gain trim")
|
|
|
|
|
|
self.gain_knob.setStyleSheet("""
|
|
|
|
|
|
QDial {
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
color: #8890a0;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
root.addWidget(self.gain_knob, alignment=Qt.AlignCenter)
|
|
|
|
|
|
|
|
|
|
|
|
gain_label = QLabel("GAIN")
|
|
|
|
|
|
gain_label.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
|
|
|
|
|
gain_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
gain_label.setStyleSheet("color: #556670; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(gain_label)
|
|
|
|
|
|
|
2026-05-13 20:53:15 +10:00
|
|
|
|
meter_row = QHBoxLayout()
|
|
|
|
|
|
meter_row.setSpacing(2)
|
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self.vu_meter = VUMeter()
|
2026-05-13 20:53:15 +10:00
|
|
|
|
meter_row.addWidget(self.vu_meter, stretch=1)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
self.volume_slider = QSlider(Qt.Vertical)
|
|
|
|
|
|
self.volume_slider.setRange(0, 100)
|
2026-05-13 20:53:15 +10:00
|
|
|
|
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.volume_slider.setStyleSheet("""
|
|
|
|
|
|
QSlider::groove:vertical {
|
2026-05-13 16:47:02 +10:00
|
|
|
|
background: #22242a;
|
|
|
|
|
|
width: 4px;
|
|
|
|
|
|
border-radius: 2px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
QSlider::handle:vertical {
|
2026-05-13 16:47:02 +10:00
|
|
|
|
background: #8890a0;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
height: 12px;
|
2026-05-13 16:47:02 +10:00
|
|
|
|
width: 14px;
|
|
|
|
|
|
margin: -4px -5px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
2026-05-13 16:47:02 +10:00
|
|
|
|
QSlider::handle:vertical:hover { background: #b0b8c8; }
|
2026-05-13 20:53:15 +10:00
|
|
|
|
QSlider::add-page:vertical {
|
2026-05-13 16:47:02 +10:00
|
|
|
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
|
|
|
|
stop:0 #00cc66, stop:1 #008844);
|
|
|
|
|
|
border-radius: 2px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
2026-05-13 20:53:15 +10:00
|
|
|
|
QSlider::sub-page:vertical {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
2026-05-12 19:29:16 +10:00
|
|
|
|
""")
|
2026-05-13 20:53:15 +10:00
|
|
|
|
meter_row.addWidget(self.volume_slider, stretch=1)
|
|
|
|
|
|
|
|
|
|
|
|
root.addLayout(meter_row, stretch=1)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
btn_row = QHBoxLayout()
|
2026-05-13 16:47:02 +10:00
|
|
|
|
btn_row.setSpacing(3)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
2026-05-15 08:41:40 +10:00
|
|
|
|
is_mic = self.channel_index < 2
|
|
|
|
|
|
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.mute_btn = QPushButton()
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self.mute_btn.setFixedSize(28, 24)
|
|
|
|
|
|
self.mute_btn.setIconSize(QSize(16, 16))
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.mute_btn.setIcon(icon_mute())
|
|
|
|
|
|
self.mute_btn.setCheckable(True)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self.mute_btn.setToolTip("Mute")
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.mute_btn.setStyleSheet("""
|
|
|
|
|
|
QPushButton {
|
2026-05-13 16:47:02 +10:00
|
|
|
|
background-color: #242026;
|
|
|
|
|
|
border: 1px solid #3a303a;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
QPushButton:checked {
|
2026-05-13 16:47:02 +10:00
|
|
|
|
background-color: #4a1a1a;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
border: 1px solid #ff4444;
|
|
|
|
|
|
}
|
2026-05-13 16:47:02 +10:00
|
|
|
|
QPushButton:hover { background-color: #343036; }
|
2026-05-12 19:29:16 +10:00
|
|
|
|
""")
|
2026-05-15 08:41:40 +10:00
|
|
|
|
self.mute_btn.setVisible(not is_mic)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
btn_row.addWidget(self.mute_btn)
|
|
|
|
|
|
|
2026-05-15 00:36:48 +10:00
|
|
|
|
# Mic ON/OFF button for channels 0 and 1
|
|
|
|
|
|
self.mic_btn = QPushButton()
|
|
|
|
|
|
self.mic_btn.setFixedSize(28, 24)
|
|
|
|
|
|
self.mic_btn.setIconSize(QSize(16, 16))
|
|
|
|
|
|
self.mic_btn.setIcon(icon_mic("#888"))
|
|
|
|
|
|
self.mic_btn.setCheckable(True)
|
|
|
|
|
|
self.mic_btn.setToolTip("Mic ON – audio goes to main")
|
|
|
|
|
|
self.mic_btn.setStyleSheet("""
|
|
|
|
|
|
QPushButton {
|
|
|
|
|
|
background-color: #202426;
|
|
|
|
|
|
border: 1px solid #303a3a;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
QPushButton:checked {
|
|
|
|
|
|
background-color: #1a3a1a;
|
|
|
|
|
|
border: 1px solid #00cc66;
|
|
|
|
|
|
}
|
|
|
|
|
|
QPushButton:hover { background-color: #303436; }
|
|
|
|
|
|
""")
|
2026-05-15 08:41:40 +10:00
|
|
|
|
self.mic_btn.setVisible(is_mic)
|
2026-05-15 00:36:48 +10:00
|
|
|
|
btn_row.addWidget(self.mic_btn)
|
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self.pfl_btn = QPushButton("PFL")
|
|
|
|
|
|
self.pfl_btn.setFixedSize(28, 24)
|
2026-05-15 08:41:40 +10:00
|
|
|
|
self.pfl_btn.setFont(QFont("Segoe UI", 8, QFont.Bold))
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self.pfl_btn.setCheckable(True)
|
|
|
|
|
|
self.pfl_btn.setToolTip("Pre-Fader Listen")
|
|
|
|
|
|
self.pfl_btn.setStyleSheet("""
|
|
|
|
|
|
QPushButton {
|
|
|
|
|
|
background-color: #202426;
|
|
|
|
|
|
border: 1px solid #303a3a;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
color: #668;
|
|
|
|
|
|
}
|
|
|
|
|
|
QPushButton:checked {
|
|
|
|
|
|
background-color: #1a3a4a;
|
|
|
|
|
|
border: 1px solid #00aaff;
|
|
|
|
|
|
color: #00ccff;
|
|
|
|
|
|
}
|
|
|
|
|
|
QPushButton:hover { background-color: #303436; }
|
|
|
|
|
|
""")
|
|
|
|
|
|
btn_row.addWidget(self.pfl_btn)
|
|
|
|
|
|
|
2026-05-12 19:29:16 +10:00
|
|
|
|
root.addLayout(btn_row)
|
|
|
|
|
|
|
|
|
|
|
|
def _connect_signals(self):
|
|
|
|
|
|
self.name_btn.clicked.connect(self._rename)
|
2026-05-15 16:42:10 +10:00
|
|
|
|
self.gain_knob.valueChanged.connect(self._on_gain_changed)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.volume_slider.valueChanged.connect(self._on_volume_changed)
|
|
|
|
|
|
self.mute_btn.toggled.connect(self._on_mute_toggled)
|
2026-05-15 00:36:48 +10:00
|
|
|
|
self.mic_btn.toggled.connect(self._on_mic_toggled)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self.pfl_btn.toggled.connect(self._on_pfl_toggled)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
def _rename(self):
|
|
|
|
|
|
name, ok = QInputDialog.getText(
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self, "Rename Channel", "Channel name:",
|
2026-05-12 19:29:16 +10:00
|
|
|
|
text=self._channel.name
|
|
|
|
|
|
)
|
|
|
|
|
|
if ok and name.strip():
|
|
|
|
|
|
self._channel.name = name.strip()
|
|
|
|
|
|
self.name_btn.setText(self._channel.name)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_volume_changed(self, value: int):
|
2026-05-13 20:53:15 +10:00
|
|
|
|
vol = (value / 100.0) ** 2
|
|
|
|
|
|
self.engine.set_channel_volume(self.channel_index, vol)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
2026-05-15 16:42:10 +10:00
|
|
|
|
def _on_gain_changed(self, value: int):
|
|
|
|
|
|
gain = value / 50.0
|
|
|
|
|
|
self.engine.set_channel_gain(self.channel_index, gain)
|
|
|
|
|
|
|
2026-05-12 19:29:16 +10:00
|
|
|
|
def _on_mute_toggled(self, checked: bool):
|
|
|
|
|
|
self.engine.set_channel_mute(self.channel_index, checked)
|
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
|
def _on_pfl_toggled(self, checked: bool):
|
|
|
|
|
|
self.engine.set_channel_pfl(self.channel_index, checked)
|
|
|
|
|
|
|
2026-05-15 00:36:48 +10:00
|
|
|
|
def _on_mic_toggled(self, checked: bool):
|
|
|
|
|
|
self.engine.set_channel_mic_on(self.channel_index, checked)
|
|
|
|
|
|
color = "#00cc66" if checked else "#888"
|
|
|
|
|
|
self.mic_btn.setIcon(icon_mic(color))
|
|
|
|
|
|
|
2026-05-12 19:29:16 +10:00
|
|
|
|
def _refresh(self):
|
|
|
|
|
|
self.vu_meter.set_value(self._channel.vu_peak)
|
2026-05-13 20:53:15 +10:00
|
|
|
|
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
2026-05-15 16:42:10 +10:00
|
|
|
|
self.gain_knob.setValue(int(self._channel.gain * 50))
|
2026-05-15 00:36:48 +10:00
|
|
|
|
if self.channel_index < 2:
|
|
|
|
|
|
color = "#00cc66" if self._channel.mic_on else "#888"
|
|
|
|
|
|
self.mic_btn.setIcon(icon_mic(color))
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-14 22:25:10 +10:00
|
|
|
|
class MasterPreSection(QFrame):
|
2026-05-12 19:29:16 +10:00
|
|
|
|
def __init__(self, engine, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.engine = engine
|
|
|
|
|
|
self._master = engine.mixer['master']
|
|
|
|
|
|
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self.setFixedWidth(80)
|
|
|
|
|
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.setStyleSheet("""
|
2026-05-14 22:25:10 +10:00
|
|
|
|
MasterPreSection {
|
|
|
|
|
|
background-color: #101216;
|
|
|
|
|
|
border: 1px solid #1e2026;
|
2026-05-13 16:47:02 +10:00
|
|
|
|
border-radius: 6px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
self._build_ui()
|
|
|
|
|
|
|
|
|
|
|
|
self.timer = QTimer(self)
|
2026-05-13 20:53:15 +10:00
|
|
|
|
self.timer.setInterval(30)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
self.timer.timeout.connect(self._refresh)
|
|
|
|
|
|
self.timer.start()
|
|
|
|
|
|
|
|
|
|
|
|
def _build_ui(self):
|
|
|
|
|
|
root = QVBoxLayout(self)
|
2026-05-14 22:25:10 +10:00
|
|
|
|
root.setContentsMargins(4, 6, 4, 6)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
root.setSpacing(4)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
header = QLabel("MASTER")
|
2026-05-15 08:41:40 +10:00
|
|
|
|
header.setFont(QFont("Segoe UI", 10, QFont.Bold))
|
2026-05-12 19:29:16 +10:00
|
|
|
|
header.setAlignment(Qt.AlignCenter)
|
2026-05-14 22:25:10 +10:00
|
|
|
|
header.setStyleSheet("color: #888; background: transparent; border: none;")
|
2026-05-12 19:29:16 +10:00
|
|
|
|
root.addWidget(header)
|
|
|
|
|
|
|
2026-05-14 22:25:10 +10:00
|
|
|
|
meter_row = QHBoxLayout()
|
|
|
|
|
|
meter_row.setSpacing(2)
|
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
|
self.vu_meter = VUMeter()
|
2026-05-14 22:25:10 +10:00
|
|
|
|
meter_row.addWidget(self.vu_meter, stretch=1)
|
|
|
|
|
|
|
|
|
|
|
|
self.volume_slider = QSlider(Qt.Vertical)
|
|
|
|
|
|
self.volume_slider.setRange(0, 100)
|
|
|
|
|
|
self.volume_slider.setValue(int(math.sqrt(self._master.volume) * 100))
|
|
|
|
|
|
self.volume_slider.setStyleSheet("""
|
|
|
|
|
|
QSlider::groove:vertical { background: #22242a; width: 4px; border-radius: 2px; }
|
|
|
|
|
|
QSlider::handle:vertical { background: #8890a0; height: 12px; width: 14px; margin: -4px -5px; border-radius: 2px; }
|
|
|
|
|
|
QSlider::handle:vertical:hover { background: #b0b8c8; }
|
|
|
|
|
|
QSlider::add-page:vertical {
|
|
|
|
|
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #00cc66, stop:1 #008844);
|
2026-05-12 19:29:16 +10:00
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
2026-05-14 22:25:10 +10:00
|
|
|
|
QSlider::sub-page:vertical { background: transparent; border-radius: 2px; }
|
|
|
|
|
|
""")
|
|
|
|
|
|
meter_row.addWidget(self.volume_slider, stretch=1)
|
|
|
|
|
|
|
|
|
|
|
|
root.addLayout(meter_row, stretch=1)
|
|
|
|
|
|
|
|
|
|
|
|
self.volume_slider.valueChanged.connect(self._on_volume_changed)
|
|
|
|
|
|
|
2026-05-15 16:42:10 +10:00
|
|
|
|
# PFL / Headphone blend
|
|
|
|
|
|
self.blend_label = QLabel("PFL⇄HP")
|
|
|
|
|
|
self.blend_label.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
|
|
|
|
|
self.blend_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
self.blend_label.setStyleSheet("color: #556670; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(self.blend_label)
|
|
|
|
|
|
|
|
|
|
|
|
self.pfl_hp_knob = QDial()
|
|
|
|
|
|
self.pfl_hp_knob.setRange(0, 100)
|
|
|
|
|
|
self.pfl_hp_knob.setValue(int(self._master.pfl_hp_blend * 100))
|
|
|
|
|
|
self.pfl_hp_knob.setFixedSize(36, 36)
|
|
|
|
|
|
self.pfl_hp_knob.setNotchesVisible(True)
|
|
|
|
|
|
self.pfl_hp_knob.setWrapping(False)
|
|
|
|
|
|
self.pfl_hp_knob.setToolTip("Left: PFL only | Centre: equal blend | Right: HP only")
|
|
|
|
|
|
self.pfl_hp_knob.setStyleSheet("""
|
|
|
|
|
|
QDial {
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
color: #8890a0;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
root.addWidget(self.pfl_hp_knob, alignment=Qt.AlignCenter)
|
|
|
|
|
|
|
|
|
|
|
|
self.pfl_hp_knob.valueChanged.connect(self._on_blend_changed)
|
|
|
|
|
|
|
2026-05-14 22:25:10 +10:00
|
|
|
|
def _on_volume_changed(self, value: int):
|
|
|
|
|
|
vol = (value / 100.0) ** 2
|
|
|
|
|
|
self._master.volume = vol
|
|
|
|
|
|
|
2026-05-15 16:42:10 +10:00
|
|
|
|
def _on_blend_changed(self, value: int):
|
|
|
|
|
|
self._master.pfl_hp_blend = value / 100.0
|
|
|
|
|
|
|
2026-05-14 22:25:10 +10:00
|
|
|
|
def _refresh(self):
|
|
|
|
|
|
self.vu_meter.set_value(self._master.vu_peak)
|
|
|
|
|
|
self.volume_slider.setValue(int(math.sqrt(self._master.volume) * 100))
|
2026-05-15 16:42:10 +10:00
|
|
|
|
self.pfl_hp_knob.setValue(int(self._master.pfl_hp_blend * 100))
|
|
|
|
|
|
|
2026-05-14 22:25:10 +10:00
|
|
|
|
|
2026-05-15 16:42:10 +10:00
|
|
|
|
class LUFSExpandedSection(QFrame):
|
2026-05-14 22:25:10 +10:00
|
|
|
|
def __init__(self, engine, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.engine = engine
|
|
|
|
|
|
self._master = engine.mixer['master']
|
|
|
|
|
|
|
2026-07-17 20:44:15 +10:00
|
|
|
|
self.setMinimumWidth(160)
|
|
|
|
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self.setStyleSheet("""
|
2026-05-15 16:42:10 +10:00
|
|
|
|
LUFSExpandedSection {
|
2026-05-14 22:25:10 +10:00
|
|
|
|
background-color: #181a1e;
|
|
|
|
|
|
border: 1px solid #282a30;
|
|
|
|
|
|
border-radius: 6px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self._build_ui()
|
|
|
|
|
|
|
|
|
|
|
|
self.timer = QTimer(self)
|
2026-05-15 16:42:10 +10:00
|
|
|
|
self.timer.setInterval(100)
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self.timer.timeout.connect(self._refresh)
|
|
|
|
|
|
self.timer.start()
|
|
|
|
|
|
|
|
|
|
|
|
def _build_ui(self):
|
|
|
|
|
|
root = QVBoxLayout(self)
|
2026-05-15 16:42:10 +10:00
|
|
|
|
root.setContentsMargins(8, 12, 8, 12)
|
|
|
|
|
|
root.setSpacing(4)
|
2026-05-14 22:25:10 +10:00
|
|
|
|
|
2026-05-15 16:42:10 +10:00
|
|
|
|
header = QLabel("LUFS")
|
2026-05-15 08:41:40 +10:00
|
|
|
|
header.setFont(QFont("Segoe UI", 10, QFont.Bold))
|
2026-05-14 22:25:10 +10:00
|
|
|
|
header.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
header.setStyleSheet("color: #556670; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(header)
|
|
|
|
|
|
|
2026-05-16 22:41:25 +10:00
|
|
|
|
self.lufs_short = QLabel("-70.0")
|
|
|
|
|
|
self.lufs_short.setFont(QFont("Segoe UI", 28, QFont.Bold))
|
|
|
|
|
|
self.lufs_short.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
self.lufs_short.setStyleSheet("color: #00cc88; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(self.lufs_short, stretch=1)
|
|
|
|
|
|
|
|
|
|
|
|
short_hint = QLabel("4 s")
|
|
|
|
|
|
short_hint.setFont(QFont("Segoe UI", 7))
|
|
|
|
|
|
short_hint.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
short_hint.setStyleSheet("color: #445; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(short_hint)
|
|
|
|
|
|
|
|
|
|
|
|
self.lufs_long = QLabel("-70.0")
|
|
|
|
|
|
self.lufs_long.setFont(QFont("Segoe UI", 18, QFont.Bold))
|
|
|
|
|
|
self.lufs_long.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
self.lufs_long.setStyleSheet("color: #5588aa; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(self.lufs_long)
|
|
|
|
|
|
|
|
|
|
|
|
long_hint = QLabel("10 min")
|
|
|
|
|
|
long_hint.setFont(QFont("Segoe UI", 7))
|
|
|
|
|
|
long_hint.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
long_hint.setStyleSheet("color: #445; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(long_hint)
|
2026-05-14 22:25:10 +10:00
|
|
|
|
|
2026-05-15 16:42:10 +10:00
|
|
|
|
self.pfl_indicator = QLabel("PFL\n\u2014")
|
|
|
|
|
|
self.pfl_indicator.setFont(QFont("Segoe UI", 8))
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self.pfl_indicator.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
self.pfl_indicator.setStyleSheet("color: #556; background: transparent; border: none;")
|
2026-05-15 16:42:10 +10:00
|
|
|
|
root.addWidget(self.pfl_indicator)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
def _refresh(self):
|
2026-05-16 22:41:25 +10:00
|
|
|
|
self.lufs_short.setText(f"{self._master.lufs_short:.1f}")
|
|
|
|
|
|
self.lufs_long.setText(f"{self._master.lufs_long:.1f}")
|
2026-05-14 22:25:10 +10:00
|
|
|
|
active_pfl = [ch for ch in self.engine.mixer['channels'] if ch.pfl]
|
|
|
|
|
|
if active_pfl:
|
|
|
|
|
|
names = ", ".join(ch.name[:3] for ch in active_pfl)
|
|
|
|
|
|
self.pfl_indicator.setText(f"PFL\n{names}")
|
|
|
|
|
|
self.pfl_indicator.setStyleSheet("color: #00aaff; background: transparent; border: none;")
|
|
|
|
|
|
else:
|
2026-05-15 16:42:10 +10:00
|
|
|
|
self.pfl_indicator.setText("PFL\n\u2014")
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self.pfl_indicator.setStyleSheet("color: #556; background: transparent; border: none;")
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
2026-05-15 16:42:10 +10:00
|
|
|
|
def apply_theme(self, theme: dict):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 20:44:15 +10:00
|
|
|
|
class MicTimerWidget(QFrame):
|
|
|
|
|
|
def __init__(self, engine, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.engine = engine
|
|
|
|
|
|
|
|
|
|
|
|
self.setMinimumWidth(120)
|
|
|
|
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
|
|
|
|
self.setStyleSheet("""
|
|
|
|
|
|
MicTimerWidget {
|
|
|
|
|
|
background-color: #181a1e;
|
|
|
|
|
|
border: 1px solid #282a30;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
root = QVBoxLayout(self)
|
|
|
|
|
|
root.setContentsMargins(8, 12, 8, 12)
|
|
|
|
|
|
root.setSpacing(4)
|
|
|
|
|
|
|
|
|
|
|
|
header = QLabel("MIC ON TIME")
|
|
|
|
|
|
header.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
|
|
|
|
|
header.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
header.setStyleSheet("color: #556670; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(header)
|
|
|
|
|
|
|
|
|
|
|
|
self.duration_label = QLabel("00:00")
|
|
|
|
|
|
self.duration_label.setFont(QFont("Courier New", 28, QFont.Bold))
|
|
|
|
|
|
self.duration_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
self.duration_label.setStyleSheet("color: #00cc66; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(self.duration_label, stretch=1)
|
|
|
|
|
|
|
|
|
|
|
|
self.status_label = QLabel("MIC OFF")
|
|
|
|
|
|
self.status_label.setFont(QFont("Segoe UI", 10, QFont.Bold))
|
|
|
|
|
|
self.status_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
self.status_label.setStyleSheet("color: #444; background: transparent; border: none;")
|
|
|
|
|
|
root.addWidget(self.status_label)
|
|
|
|
|
|
|
|
|
|
|
|
self.timer = QTimer(self)
|
|
|
|
|
|
self.timer.setInterval(100)
|
|
|
|
|
|
self.timer.timeout.connect(self._refresh)
|
|
|
|
|
|
self.timer.start()
|
|
|
|
|
|
|
|
|
|
|
|
def _refresh(self):
|
|
|
|
|
|
dur = self.engine.get_mic_duration()
|
|
|
|
|
|
channels = self.engine.mixer['channels']
|
|
|
|
|
|
any_on = any(ch.mic_on for ch in channels[:2])
|
|
|
|
|
|
|
|
|
|
|
|
if any_on:
|
|
|
|
|
|
mins = int(dur // 60)
|
|
|
|
|
|
secs = int(dur % 60)
|
|
|
|
|
|
self.duration_label.setText(f"{mins:02d}:{secs:02d}")
|
|
|
|
|
|
self.duration_label.setStyleSheet("color: #00ff41; background: transparent; border: none;")
|
|
|
|
|
|
self.status_label.setText("MIC LIVE")
|
|
|
|
|
|
self.status_label.setStyleSheet("color: #ff4444; font-weight: bold; background: transparent; border: none;")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.duration_label.setText("00:00")
|
|
|
|
|
|
self.duration_label.setStyleSheet("color: #333; background: transparent; border: none;")
|
|
|
|
|
|
self.status_label.setText("MIC OFF")
|
|
|
|
|
|
self.status_label.setStyleSheet("color: #444; background: transparent; border: none;")
|
|
|
|
|
|
|
|
|
|
|
|
def apply_theme(self, theme: dict):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 19:29:16 +10:00
|
|
|
|
class MixerWidget(QFrame):
|
|
|
|
|
|
def __init__(self, engine, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.engine = engine
|
|
|
|
|
|
self.setStyleSheet("""
|
2026-05-13 16:47:02 +10:00
|
|
|
|
MixerWidget {
|
|
|
|
|
|
background-color: #121318;
|
|
|
|
|
|
border: 1px solid #282a30;
|
|
|
|
|
|
border-radius: 8px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
self._build_ui()
|
|
|
|
|
|
|
|
|
|
|
|
def _build_ui(self):
|
|
|
|
|
|
root = QVBoxLayout(self)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
root.setContentsMargins(6, 4, 6, 6)
|
|
|
|
|
|
root.setSpacing(4)
|
2026-05-12 19:29:16 +10:00
|
|
|
|
|
|
|
|
|
|
header = QLabel("MIXER")
|
2026-05-15 08:41:40 +10:00
|
|
|
|
header.setFont(QFont("Segoe UI", 11, QFont.Bold))
|
2026-05-13 16:47:02 +10:00
|
|
|
|
header.setStyleSheet("color: #556670; background: transparent; border: none; "
|
|
|
|
|
|
"letter-spacing: 2px;")
|
2026-05-12 19:29:16 +10:00
|
|
|
|
root.addWidget(header)
|
|
|
|
|
|
|
|
|
|
|
|
scroll = QScrollArea()
|
|
|
|
|
|
scroll.setWidgetResizable(True)
|
|
|
|
|
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
|
|
|
|
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
|
|
|
|
scroll.setStyleSheet("""
|
|
|
|
|
|
QScrollArea {
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
QScrollBar:horizontal {
|
2026-05-13 16:47:02 +10:00
|
|
|
|
background: #1a1c22;
|
|
|
|
|
|
height: 6px;
|
|
|
|
|
|
border-radius: 3px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
QScrollBar::handle:horizontal {
|
2026-05-13 16:47:02 +10:00
|
|
|
|
background: #3a3c44;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
QScrollBar::handle:horizontal:hover { background: #5a5c64; }
|
|
|
|
|
|
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
|
|
|
|
|
|
width: 0px;
|
2026-05-12 19:29:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
scroll_content = QWidget()
|
|
|
|
|
|
scroll_content.setStyleSheet("background: transparent;")
|
|
|
|
|
|
channels_layout = QHBoxLayout(scroll_content)
|
|
|
|
|
|
channels_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
channels_layout.setSpacing(4)
|
|
|
|
|
|
|
|
|
|
|
|
self._strips = []
|
|
|
|
|
|
for i in range(NUM_MIXER_CHANNELS):
|
|
|
|
|
|
strip = ChannelStrip(self.engine, i)
|
|
|
|
|
|
self._strips.append(strip)
|
|
|
|
|
|
channels_layout.addWidget(strip)
|
|
|
|
|
|
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self.master_pre = MasterPreSection(self.engine)
|
|
|
|
|
|
channels_layout.addWidget(self.master_pre)
|
|
|
|
|
|
|
2026-05-12 19:29:16 +10:00
|
|
|
|
scroll.setWidget(scroll_content)
|
|
|
|
|
|
root.addWidget(scroll)
|
|
|
|
|
|
|
|
|
|
|
|
def apply_theme(self, theme: dict):
|
|
|
|
|
|
for strip in self._strips:
|
|
|
|
|
|
strip.apply_theme(theme)
|
2026-05-14 22:25:10 +10:00
|
|
|
|
self.master_pre.apply_theme(theme)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
|
2026-05-16 22:41:25 +10:00
|
|
|
|
def refresh_strips(self):
|
2026-05-13 16:47:02 +10:00
|
|
|
|
for strip in self._strips:
|
|
|
|
|
|
ch = strip._channel
|
|
|
|
|
|
strip.name_btn.setText(ch.name)
|
|
|
|
|
|
strip.volume_slider.setValue(int(ch.volume * 100))
|
2026-05-15 16:42:10 +10:00
|
|
|
|
strip.gain_knob.setValue(int(ch.gain * 50))
|
2026-05-13 16:47:02 +10:00
|
|
|
|
strip.mute_btn.setChecked(ch.muted)
|
|
|
|
|
|
strip.pfl_btn.setChecked(ch.pfl)
|