Fix AttributeError: remove redundant mapping_learned connection in main.py
TopBarWidget already connects mapping_learned to _on_mapping_learned internally in set_midi_engine, so the external connection in main.py was both redundant and referenced a non-existent method.
This commit is contained in:
+211
-181
@@ -3,31 +3,21 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSlider,
|
||||
QPushButton, QFrame, QScrollArea, QDial, QSizePolicy,
|
||||
QInputDialog, QCheckBox
|
||||
QInputDialog
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtCore import Qt, QTimer, QSize
|
||||
from PySide6.QtGui import QFont, QPainter, QColor, QLinearGradient, QPen
|
||||
|
||||
from audio_engine import NUM_MIXER_CHANNELS
|
||||
from icons import icon_mute, icon_solo
|
||||
|
||||
|
||||
STYLESHEET = """
|
||||
QWidget {{
|
||||
background-color: {bg};
|
||||
color: {text};
|
||||
font-family: Arial;
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
class VUMeter(QWidget):
|
||||
def __init__(self, parent=None, orientation=Qt.Vertical):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._value = 0.0
|
||||
self._orientation = orientation
|
||||
self.setMinimumWidth(24 if orientation == Qt.Vertical else 100)
|
||||
self.setMinimumHeight(100 if orientation == Qt.Vertical else 24)
|
||||
self.setMinimumWidth(28)
|
||||
self.setMinimumHeight(80)
|
||||
|
||||
def set_value(self, value: float):
|
||||
self._value = max(0.0, min(1.0, value))
|
||||
@@ -37,26 +27,32 @@ class VUMeter(QWidget):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
rect = self.rect().adjusted(2, 2, -2, -2)
|
||||
rect = self.rect().adjusted(14, 3, -3, -3)
|
||||
if rect.width() < 1 or rect.height() < 1:
|
||||
painter.end()
|
||||
return
|
||||
|
||||
# Background
|
||||
painter.fillRect(rect, QColor("#0a0a0a"))
|
||||
painter.fillRect(rect, QColor("#080808"))
|
||||
|
||||
db_markings = [(-40, "#1a1a1a"), (-30, "#1a1a1a"), (-20, "#1a1a1a"),
|
||||
(-12, "#2a2a1a"), (-6, "#2a1a1a"), (0, "#2a0a0a")]
|
||||
for db, color in db_markings:
|
||||
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(color), 1))
|
||||
painter.drawLine(rect.right() - 5, y, rect.right(), y)
|
||||
painter.setPen(QPen(QColor("#444444"), 1))
|
||||
painter.drawText(rect.left() - 13, y + 4, f"{db}")
|
||||
|
||||
painter.setPen(QPen(QColor("#1a1a1a"), 1))
|
||||
painter.drawRect(rect)
|
||||
|
||||
val = self._value
|
||||
if val < 1e-10:
|
||||
painter.setPen(QPen(QColor("#222222"), 1))
|
||||
painter.drawRect(rect)
|
||||
painter.end()
|
||||
return
|
||||
|
||||
# Scale to dB-like (log-ish): map 0.0..1.0 to -40..0 dB display
|
||||
db = -40 * (1 - val) if val < 1.0 else 3
|
||||
fill_pct = (db + 40) / 43.0 if db <= 0 else 1.0
|
||||
fill_pct = max(0.0, min(1.0, fill_pct))
|
||||
|
||||
gradient = QLinearGradient(0, rect.bottom(), 0, rect.top())
|
||||
gradient.setColorAt(0.0, QColor("#003300"))
|
||||
gradient.setColorAt(0.5, QColor("#00cc00"))
|
||||
@@ -64,14 +60,21 @@ class VUMeter(QWidget):
|
||||
gradient.setColorAt(0.9, QColor("#ff6600"))
|
||||
gradient.setColorAt(1.0, QColor("#ff0000"))
|
||||
|
||||
fill_h = int(rect.height() * fill_pct)
|
||||
painter.fillRect(
|
||||
rect.x(), rect.bottom() - fill_h,
|
||||
rect.width(), fill_h, gradient
|
||||
)
|
||||
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))
|
||||
|
||||
fill_h = int(rect.height() * fill_pct)
|
||||
painter.fillRect(rect.x(), rect.bottom() - fill_h,
|
||||
rect.width(), fill_h, gradient)
|
||||
|
||||
for db, _ in db_markings:
|
||||
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)
|
||||
|
||||
painter.setPen(QPen(QColor("#333333"), 1))
|
||||
painter.drawRect(rect)
|
||||
painter.end()
|
||||
|
||||
|
||||
@@ -82,13 +85,13 @@ class ChannelStrip(QFrame):
|
||||
self.channel_index = channel_index
|
||||
self._channel = engine.get_channel(channel_index)
|
||||
|
||||
self.setFixedWidth(110)
|
||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
||||
self.setMinimumWidth(90)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #151515;
|
||||
border: 1px solid #333333;
|
||||
border-radius: 4px;
|
||||
ChannelStrip {
|
||||
background-color: #181a1e;
|
||||
border: 1px solid #282a30;
|
||||
border-radius: 6px;
|
||||
}
|
||||
""")
|
||||
|
||||
@@ -105,109 +108,117 @@ class ChannelStrip(QFrame):
|
||||
root.setContentsMargins(4, 6, 4, 6)
|
||||
root.setSpacing(4)
|
||||
|
||||
# Name label
|
||||
self.name_btn = QPushButton(self._channel.name)
|
||||
self.name_btn.setFont(QFont("Arial", 9, QFont.Bold))
|
||||
self.name_btn.setFont(QFont("Segoe UI", 8, QFont.Bold))
|
||||
self.name_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
color: #aaaaaa;
|
||||
color: #999aaa;
|
||||
border: none;
|
||||
padding: 2px;
|
||||
font-size: 8pt;
|
||||
}
|
||||
QPushButton:hover {
|
||||
color: #ffffff;
|
||||
background-color: #222222;
|
||||
background-color: #22252c;
|
||||
border-radius: 3px;
|
||||
}
|
||||
""")
|
||||
root.addWidget(self.name_btn)
|
||||
|
||||
# VU Meter
|
||||
self.vu_meter = VUMeter(orientation=Qt.Vertical)
|
||||
self.vu_meter.setMinimumHeight(120)
|
||||
self.vu_meter = VUMeter()
|
||||
root.addWidget(self.vu_meter, stretch=1)
|
||||
|
||||
# Volume slider
|
||||
vol_row = QHBoxLayout()
|
||||
vol_row.setSpacing(2)
|
||||
vol_label = QLabel("V")
|
||||
vol_label.setFont(QFont("Arial", 8))
|
||||
vol_label.setStyleSheet("color: #666666; background: transparent; border: none;")
|
||||
vol_row.addWidget(vol_label)
|
||||
|
||||
self.volume_slider = QSlider(Qt.Vertical)
|
||||
self.volume_slider.setRange(0, 100)
|
||||
self.volume_slider.setValue(100)
|
||||
self.volume_slider.setFixedHeight(80)
|
||||
self.volume_slider.setFixedHeight(60)
|
||||
self.volume_slider.setStyleSheet("""
|
||||
QSlider::groove:vertical {
|
||||
background: #222222;
|
||||
width: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QSlider::handle:vertical {
|
||||
background: #666666;
|
||||
height: 12px;
|
||||
width: 16px;
|
||||
margin: -4px -4px;
|
||||
background: #22242a;
|
||||
width: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
QSlider::handle:vertical:hover {
|
||||
background: #aaaaaa;
|
||||
QSlider::handle:vertical {
|
||||
background: #8890a0;
|
||||
height: 12px;
|
||||
width: 14px;
|
||||
margin: -4px -5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
QSlider::handle:vertical:hover { background: #b0b8c8; }
|
||||
QSlider::sub-page:vertical {
|
||||
background: #00aa44;
|
||||
border-radius: 3px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #00cc66, stop:1 #008844);
|
||||
border-radius: 2px;
|
||||
}
|
||||
""")
|
||||
vol_row.addWidget(self.volume_slider, alignment=Qt.AlignCenter)
|
||||
root.addLayout(vol_row)
|
||||
root.addWidget(self.volume_slider, alignment=Qt.AlignCenter)
|
||||
|
||||
# Mute / Solo buttons
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setSpacing(4)
|
||||
btn_row.setSpacing(3)
|
||||
|
||||
self.mute_btn = QPushButton()
|
||||
self.mute_btn.setFixedSize(32, 28)
|
||||
self.mute_btn.setIconSize(self.mute_btn.size() * 0.7)
|
||||
self.mute_btn.setFixedSize(28, 24)
|
||||
self.mute_btn.setIconSize(QSize(16, 16))
|
||||
self.mute_btn.setIcon(icon_mute())
|
||||
self.mute_btn.setCheckable(True)
|
||||
self.mute_btn.setToolTip("Mute")
|
||||
self.mute_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2a1a1a;
|
||||
border: 1px solid #4a2a2a;
|
||||
background-color: #242026;
|
||||
border: 1px solid #3a303a;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #5a2020;
|
||||
background-color: #4a1a1a;
|
||||
border: 1px solid #ff4444;
|
||||
}
|
||||
QPushButton:hover { background-color: #3a2a2a; }
|
||||
QPushButton:hover { background-color: #343036; }
|
||||
""")
|
||||
self.mute_btn.setToolTip("Mute")
|
||||
btn_row.addWidget(self.mute_btn)
|
||||
|
||||
self.solo_btn = QPushButton()
|
||||
self.solo_btn.setFixedSize(32, 28)
|
||||
self.solo_btn.setIconSize(self.solo_btn.size() * 0.7)
|
||||
self.solo_btn.setFixedSize(28, 24)
|
||||
self.solo_btn.setIconSize(QSize(16, 16))
|
||||
self.solo_btn.setIcon(icon_solo())
|
||||
self.solo_btn.setCheckable(True)
|
||||
self.solo_btn.setToolTip("Solo")
|
||||
self.solo_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2a2a1a;
|
||||
border: 1px solid #4a4a2a;
|
||||
background-color: #262420;
|
||||
border: 1px solid #3a3830;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #5a5a20;
|
||||
background-color: #4a4a1a;
|
||||
border: 1px solid #ffaa00;
|
||||
}
|
||||
QPushButton:hover { background-color: #3a3a2a; }
|
||||
QPushButton:hover { background-color: #363430; }
|
||||
""")
|
||||
self.solo_btn.setToolTip("Solo")
|
||||
btn_row.addWidget(self.solo_btn)
|
||||
|
||||
self.pfl_btn = QPushButton("PFL")
|
||||
self.pfl_btn.setFixedSize(28, 24)
|
||||
self.pfl_btn.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||
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)
|
||||
|
||||
root.addLayout(btn_row)
|
||||
|
||||
def _connect_signals(self):
|
||||
@@ -215,11 +226,11 @@ class ChannelStrip(QFrame):
|
||||
self.volume_slider.valueChanged.connect(self._on_volume_changed)
|
||||
self.mute_btn.toggled.connect(self._on_mute_toggled)
|
||||
self.solo_btn.toggled.connect(self._on_solo_toggled)
|
||||
self.pfl_btn.toggled.connect(self._on_pfl_toggled)
|
||||
|
||||
def _rename(self):
|
||||
name, ok = QInputDialog.getText(
|
||||
self, "Rename Channel",
|
||||
"Channel name:",
|
||||
self, "Rename Channel", "Channel name:",
|
||||
text=self._channel.name
|
||||
)
|
||||
if ok and name.strip():
|
||||
@@ -235,6 +246,9 @@ class ChannelStrip(QFrame):
|
||||
def _on_solo_toggled(self, checked: bool):
|
||||
self.engine.set_channel_solo(self.channel_index, checked)
|
||||
|
||||
def _on_pfl_toggled(self, checked: bool):
|
||||
self.engine.set_channel_pfl(self.channel_index, checked)
|
||||
|
||||
def _refresh(self):
|
||||
self.vu_meter.set_value(self._channel.vu_peak)
|
||||
|
||||
@@ -248,13 +262,13 @@ class MasterSection(QFrame):
|
||||
self.engine = engine
|
||||
self._master = engine.mixer['master']
|
||||
|
||||
self.setFixedWidth(180)
|
||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
||||
self.setMinimumWidth(180)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #151515;
|
||||
border: 1px solid #333333;
|
||||
border-radius: 4px;
|
||||
MasterSection {
|
||||
background-color: #181a1e;
|
||||
border: 1px solid #282a30;
|
||||
border-radius: 6px;
|
||||
}
|
||||
""")
|
||||
|
||||
@@ -268,70 +282,72 @@ class MasterSection(QFrame):
|
||||
|
||||
def _build_ui(self):
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(6, 6, 6, 6)
|
||||
root.setSpacing(6)
|
||||
root.setContentsMargins(8, 6, 8, 6)
|
||||
root.setSpacing(4)
|
||||
|
||||
# Header
|
||||
header = QLabel("MASTER")
|
||||
header.setFont(QFont("Arial", 10, QFont.Bold))
|
||||
header.setFont(QFont("Segoe UI", 10, QFont.Bold))
|
||||
header.setAlignment(Qt.AlignCenter)
|
||||
header.setStyleSheet("color: #00ff41; background: transparent; border: none;")
|
||||
header.setStyleSheet("color: #00cc88; background: transparent; border: none;")
|
||||
root.addWidget(header)
|
||||
|
||||
# VU Meter
|
||||
self.vu_meter = VUMeter(orientation=Qt.Vertical)
|
||||
self.vu_meter.setMinimumHeight(100)
|
||||
self.vu_meter = VUMeter()
|
||||
root.addWidget(self.vu_meter)
|
||||
|
||||
# LUFS readout
|
||||
self.lufs_label = QLabel("-70.0 LUFS")
|
||||
self.lufs_label.setFont(QFont("Courier New", 10, QFont.Bold))
|
||||
self.lufs_label.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
||||
self.lufs_label.setAlignment(Qt.AlignCenter)
|
||||
self.lufs_label.setStyleSheet("color: #00ff41; background: transparent; border: none;")
|
||||
self.lufs_label.setStyleSheet("color: #00cc88; background: transparent; border: none;")
|
||||
root.addWidget(self.lufs_label)
|
||||
|
||||
# Compressor section
|
||||
pfl_indicator = QLabel("PFL: —")
|
||||
self.pfl_indicator = pfl_indicator
|
||||
pfl_indicator.setFont(QFont("Segoe UI", 8))
|
||||
pfl_indicator.setAlignment(Qt.AlignCenter)
|
||||
pfl_indicator.setStyleSheet("color: #556; background: transparent; border: none;")
|
||||
root.addWidget(pfl_indicator)
|
||||
|
||||
comp_frame = QFrame()
|
||||
comp_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #111111;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 3px;
|
||||
background-color: #121318;
|
||||
border: 1px solid #22242a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
comp_layout = QVBoxLayout(comp_frame)
|
||||
comp_layout.setContentsMargins(4, 4, 4, 4)
|
||||
comp_layout.setSpacing(3)
|
||||
comp_layout.setSpacing(2)
|
||||
|
||||
comp_header_row = QHBoxLayout()
|
||||
comp_label = QLabel("COMPRESSOR")
|
||||
comp_label.setFont(QFont("Arial", 8, QFont.Bold))
|
||||
comp_label.setStyleSheet("color: #888888; background: transparent; border: none;")
|
||||
comp_header_row.addWidget(comp_label)
|
||||
comp_header = QHBoxLayout()
|
||||
cl = QLabel("COMPRESSOR")
|
||||
cl.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||
cl.setStyleSheet("color: #778; background: transparent; border: none;")
|
||||
comp_header.addWidget(cl)
|
||||
|
||||
self.comp_bypass = QPushButton("B")
|
||||
self.comp_bypass.setFixedSize(24, 20)
|
||||
self.comp_bypass.setFixedSize(22, 18)
|
||||
self.comp_bypass.setCheckable(True)
|
||||
self.comp_bypass.setFont(QFont("Arial", 8, QFont.Bold))
|
||||
self.comp_bypass.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||
self.comp_bypass.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2a2a2a;
|
||||
color: #888888;
|
||||
border: 1px solid #444;
|
||||
background-color: #22242a;
|
||||
color: #778;
|
||||
border: 1px solid #333;
|
||||
border-radius: 2px;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #5a2020;
|
||||
background-color: #4a1a1a;
|
||||
color: #ff6666;
|
||||
border: 1px solid #ff4444;
|
||||
}
|
||||
""")
|
||||
self.comp_bypass.setToolTip("Bypass compressor")
|
||||
comp_header_row.addWidget(self.comp_bypass)
|
||||
comp_layout.addLayout(comp_header_row)
|
||||
comp_header.addWidget(self.comp_bypass)
|
||||
comp_layout.addLayout(comp_header)
|
||||
|
||||
knobs_row = QHBoxLayout()
|
||||
knobs_row.setSpacing(2)
|
||||
knobs = QHBoxLayout()
|
||||
knobs.setSpacing(4)
|
||||
for label, attr, default, lo, hi, suffix in [
|
||||
("THR", "threshold_db", -20, -60, 0, "dB"),
|
||||
("RAT", "ratio", 4, 1, 20, ":1"),
|
||||
@@ -339,60 +355,58 @@ class MasterSection(QFrame):
|
||||
("REL", "release_ms", 100, 10, 1000, "ms"),
|
||||
("MKG", "makeup_gain_db", 0, 0, 24, "dB"),
|
||||
]:
|
||||
k = self._make_knob(label, self._master.compressor, attr, default, lo, hi, suffix)
|
||||
knobs_row.addWidget(k)
|
||||
comp_layout.addLayout(knobs_row)
|
||||
|
||||
k = self._make_knob(label, self._master.compressor, attr,
|
||||
default, lo, hi, suffix)
|
||||
knobs.addWidget(k)
|
||||
comp_layout.addLayout(knobs)
|
||||
root.addWidget(comp_frame)
|
||||
|
||||
# Limiter section
|
||||
lim_frame = QFrame()
|
||||
lim_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #111111;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 3px;
|
||||
background-color: #121318;
|
||||
border: 1px solid #22242a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
lim_layout = QVBoxLayout(lim_frame)
|
||||
lim_layout.setContentsMargins(4, 4, 4, 4)
|
||||
lim_layout.setSpacing(3)
|
||||
lim_layout.setSpacing(2)
|
||||
|
||||
lim_header_row = QHBoxLayout()
|
||||
lim_label = QLabel("LIMITER")
|
||||
lim_label.setFont(QFont("Arial", 8, QFont.Bold))
|
||||
lim_label.setStyleSheet("color: #888888; background: transparent; border: none;")
|
||||
lim_header_row.addWidget(lim_label)
|
||||
lim_header = QHBoxLayout()
|
||||
ll = QLabel("LIMITER")
|
||||
ll.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||
ll.setStyleSheet("color: #778; background: transparent; border: none;")
|
||||
lim_header.addWidget(ll)
|
||||
|
||||
self.lim_bypass = QPushButton("B")
|
||||
self.lim_bypass.setFixedSize(24, 20)
|
||||
self.lim_bypass.setFixedSize(22, 18)
|
||||
self.lim_bypass.setCheckable(True)
|
||||
self.lim_bypass.setFont(QFont("Arial", 8, QFont.Bold))
|
||||
self.lim_bypass.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||
self.lim_bypass.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2a2a2a;
|
||||
color: #888888;
|
||||
border: 1px solid #444;
|
||||
background-color: #22242a;
|
||||
color: #778;
|
||||
border: 1px solid #333;
|
||||
border-radius: 2px;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #5a2020;
|
||||
background-color: #4a1a1a;
|
||||
color: #ff6666;
|
||||
border: 1px solid #ff4444;
|
||||
}
|
||||
""")
|
||||
self.lim_bypass.setToolTip("Bypass limiter")
|
||||
lim_header_row.addWidget(self.lim_bypass)
|
||||
lim_layout.addLayout(lim_header_row)
|
||||
lim_header.addWidget(self.lim_bypass)
|
||||
lim_layout.addLayout(lim_header)
|
||||
|
||||
lim_knobs = QHBoxLayout()
|
||||
lim_knobs.setSpacing(2)
|
||||
k = self._make_knob("CEIL", self._master.limiter, "ceiling_db", -0.1, -10, 0, "dB")
|
||||
lim_knobs.setSpacing(4)
|
||||
k = self._make_knob("CEIL", self._master.limiter, "ceiling_db",
|
||||
-0.1, -10, 0, "dB")
|
||||
lim_knobs.addWidget(k)
|
||||
lim_layout.addLayout(lim_knobs)
|
||||
|
||||
root.addWidget(lim_frame)
|
||||
|
||||
root.addStretch()
|
||||
|
||||
def _make_knob(self, label, obj, attr, default, lo, hi, suffix):
|
||||
@@ -403,24 +417,23 @@ class MasterSection(QFrame):
|
||||
layout.setSpacing(1)
|
||||
|
||||
lbl = QLabel(label)
|
||||
lbl.setFont(QFont("Arial", 7, QFont.Bold))
|
||||
lbl.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||
lbl.setAlignment(Qt.AlignCenter)
|
||||
lbl.setStyleSheet("color: #666666; background: transparent; border: none;")
|
||||
lbl.setStyleSheet("color: #556; background: transparent; border: none;")
|
||||
layout.addWidget(lbl)
|
||||
|
||||
dial = QDial()
|
||||
dial.setFixedSize(32, 32)
|
||||
dial.setFixedSize(44, 44)
|
||||
dial.setRange(0, 1000)
|
||||
dial.setNotchesVisible(False)
|
||||
dial.setStyleSheet("""
|
||||
QDial {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 16px;
|
||||
background-color: #121318;
|
||||
border: 1px solid #2a2c33;
|
||||
border-radius: 22px;
|
||||
}
|
||||
""")
|
||||
|
||||
# Map value to dial position
|
||||
def _val_to_pos(v):
|
||||
return int((v - lo) / (hi - lo) * 1000)
|
||||
|
||||
@@ -430,9 +443,9 @@ class MasterSection(QFrame):
|
||||
dial.setValue(_val_to_pos(default))
|
||||
|
||||
val_label = QLabel(f"{default}{suffix}")
|
||||
val_label.setFont(QFont("Courier New", 8))
|
||||
val_label.setFont(QFont("Segoe UI", 10, QFont.Bold))
|
||||
val_label.setAlignment(Qt.AlignCenter)
|
||||
val_label.setStyleSheet("color: #aaaaaa; background: transparent; border: none;")
|
||||
val_label.setStyleSheet("color: #ccd; background: transparent; border: none;")
|
||||
|
||||
def _on_dial(v):
|
||||
val = _pos_to_val(v)
|
||||
@@ -454,7 +467,6 @@ class MasterSection(QFrame):
|
||||
dial.valueChanged.connect(_on_dial)
|
||||
layout.addWidget(dial, alignment=Qt.AlignCenter)
|
||||
layout.addWidget(val_label)
|
||||
|
||||
return frame
|
||||
|
||||
def _connect_signals(self):
|
||||
@@ -468,6 +480,18 @@ class MasterSection(QFrame):
|
||||
def _refresh(self):
|
||||
self.vu_meter.set_value(self._master.vu_peak)
|
||||
self.lufs_label.setText(f"{self._master.lufs_current:.1f} LUFS")
|
||||
active_pfl = [ch for ch in self.engine.mixer['channels'] if ch.pfl]
|
||||
if active_pfl:
|
||||
names = ", ".join(ch.name[:6] for ch in active_pfl)
|
||||
self.pfl_indicator.setText(f"PFL: {names}")
|
||||
self.pfl_indicator.setStyleSheet(
|
||||
"color: #00aaff; background: transparent; border: none;"
|
||||
)
|
||||
else:
|
||||
self.pfl_indicator.setText("PFL: —")
|
||||
self.pfl_indicator.setStyleSheet(
|
||||
"color: #556; background: transparent; border: none;"
|
||||
)
|
||||
|
||||
def apply_theme(self, theme: dict):
|
||||
pass
|
||||
@@ -477,13 +501,11 @@ class MixerWidget(QFrame):
|
||||
def __init__(self, engine, parent=None):
|
||||
super().__init__(parent)
|
||||
self.engine = engine
|
||||
|
||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #0d0d0d;
|
||||
border: 2px solid #333333;
|
||||
border-radius: 6px;
|
||||
MixerWidget {
|
||||
background-color: #121318;
|
||||
border: 1px solid #282a30;
|
||||
border-radius: 8px;
|
||||
}
|
||||
""")
|
||||
|
||||
@@ -491,34 +513,36 @@ class MixerWidget(QFrame):
|
||||
|
||||
def _build_ui(self):
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(8, 8, 8, 8)
|
||||
root.setSpacing(6)
|
||||
root.setContentsMargins(6, 4, 6, 6)
|
||||
root.setSpacing(4)
|
||||
|
||||
# Header
|
||||
header = QLabel("MIXER")
|
||||
header.setFont(QFont("Arial", 10, QFont.Bold))
|
||||
header.setStyleSheet("color: #666666; background: transparent; border: none;")
|
||||
header.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
||||
header.setStyleSheet("color: #556670; background: transparent; border: none; "
|
||||
"letter-spacing: 2px;")
|
||||
root.addWidget(header)
|
||||
|
||||
# Scrollable channel strips
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll.setFixedHeight(350)
|
||||
scroll.setStyleSheet("""
|
||||
QScrollArea {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
background: #1a1a1a;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: #1a1c22;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background: #444444;
|
||||
border-radius: 4px;
|
||||
background: #3a3c44;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover { background: #5a5c64; }
|
||||
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
|
||||
width: 0px;
|
||||
}
|
||||
""")
|
||||
|
||||
@@ -534,12 +558,9 @@ class MixerWidget(QFrame):
|
||||
self._strips.append(strip)
|
||||
channels_layout.addWidget(strip)
|
||||
|
||||
# Master section
|
||||
self.master_section = MasterSection(self.engine)
|
||||
channels_layout.addWidget(self.master_section)
|
||||
|
||||
channels_layout.addStretch()
|
||||
|
||||
scroll.setWidget(scroll_content)
|
||||
root.addWidget(scroll)
|
||||
|
||||
@@ -547,3 +568,12 @@ class MixerWidget(QFrame):
|
||||
for strip in self._strips:
|
||||
strip.apply_theme(theme)
|
||||
self.master_section.apply_theme(theme)
|
||||
|
||||
def refresh_strips(self):
|
||||
for strip in self._strips:
|
||||
ch = strip._channel
|
||||
strip.name_btn.setText(ch.name)
|
||||
strip.volume_slider.setValue(int(ch.volume * 100))
|
||||
strip.mute_btn.setChecked(ch.muted)
|
||||
strip.solo_btn.setChecked(ch.solo)
|
||||
strip.pfl_btn.setChecked(ch.pfl)
|
||||
Reference in New Issue
Block a user