Files
gti_radiostudio/mixer_widget.py
T
nickmin 4d6a639c08 Remove solo buttons, hide mute on mics, fix master volume, add mic_on MIDI, resize fonts
- Removed solo buttons from all mixer channels
- Mute button hidden on mic channels (0,1) — mic_on toggle replaces it
- Fixed master volume fader not being applied in audio _process()
- Added mixer_mic_on MIDI signal and learnable action
- Added CLEAR MIDI button to settings popup
- Increased settings popup fonts 7pt→9pt, popup size 460→620px
- Reduced deck fonts (header 24→16, elapsed 32→22, etc.) and spacing
- Increased mixer fonts (names 8→9, headers 9→11, etc.)
- Added missing MIDI learn entries (mute ch3-8, pfl ch3-8, mic_on ch1-2)
2026-05-15 08:41:40 +10:00

605 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# mixer_widget.py
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSlider,
QPushButton, QFrame, QScrollArea, QDial, QSizePolicy,
QInputDialog
)
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_mic
import math
class VUMeter(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._value = 0.0
self.setMinimumWidth(28)
self.setMinimumHeight(80)
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)
rect = self.rect().adjusted(18, 3, -3, -3)
if rect.width() < 1 or rect.height() < 1:
painter.end()
return
painter.fillRect(rect, QColor("#080808"))
db_markings = [(-40,), (-30,), (-20,), (-12,), (-6,), (0,)]
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)
if db == -6:
painter.setPen(QPen(QColor("#00ff88"), 2))
painter.drawLine(rect.right() - 3, y, rect.right(), y)
painter.setFont(QFont("Segoe UI", 9, QFont.Bold))
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))
painter.setFont(QFont("Segoe UI", 8))
painter.drawText(rect.left() - 16, y + 3, f"{db}")
painter.setPen(QPen(QColor("#1a1a1a"), 1))
painter.drawRect(rect)
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"))
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_pct = math.sqrt(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.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)
self.setFixedWidth(100)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
self.setStyleSheet("""
ChannelStrip {
background-color: #181a1e;
border: 1px solid #282a30;
border-radius: 6px;
}
""")
self._build_ui()
self._connect_signals()
self.timer = QTimer(self)
self.timer.setInterval(30)
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)
self.name_btn.setFont(QFont("Segoe UI", 9, QFont.Bold))
self.name_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #999aaa;
border: none;
padding: 2px;
font-size: 9pt;
}
QPushButton:hover {
color: #ffffff;
background-color: #22252c;
border-radius: 3px;
}
""")
root.addWidget(self.name_btn)
meter_row = QHBoxLayout()
meter_row.setSpacing(2)
self.vu_meter = VUMeter()
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._channel.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);
border-radius: 2px;
}
QSlider::sub-page:vertical {
background: transparent;
border-radius: 2px;
}
""")
meter_row.addWidget(self.volume_slider, stretch=1)
root.addLayout(meter_row, stretch=1)
btn_row = QHBoxLayout()
btn_row.setSpacing(3)
is_mic = self.channel_index < 2
self.mute_btn = QPushButton()
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: #242026;
border: 1px solid #3a303a;
border-radius: 3px;
}
QPushButton:checked {
background-color: #4a1a1a;
border: 1px solid #ff4444;
}
QPushButton:hover { background-color: #343036; }
""")
self.mute_btn.setVisible(not is_mic)
btn_row.addWidget(self.mute_btn)
# 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; }
""")
self.mic_btn.setVisible(is_mic)
btn_row.addWidget(self.mic_btn)
self.pfl_btn = QPushButton("PFL")
self.pfl_btn.setFixedSize(28, 24)
self.pfl_btn.setFont(QFont("Segoe UI", 8, 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):
self.name_btn.clicked.connect(self._rename)
self.volume_slider.valueChanged.connect(self._on_volume_changed)
self.mute_btn.toggled.connect(self._on_mute_toggled)
self.mic_btn.toggled.connect(self._on_mic_toggled)
self.pfl_btn.toggled.connect(self._on_pfl_toggled)
def _rename(self):
name, ok = QInputDialog.getText(
self, "Rename Channel", "Channel name:",
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):
vol = (value / 100.0) ** 2
self.engine.set_channel_volume(self.channel_index, vol)
def _on_mute_toggled(self, checked: bool):
self.engine.set_channel_mute(self.channel_index, checked)
def _on_pfl_toggled(self, checked: bool):
self.engine.set_channel_pfl(self.channel_index, checked)
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))
def _refresh(self):
self.vu_meter.set_value(self._channel.vu_peak)
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
if self.channel_index < 2:
color = "#00cc66" if self._channel.mic_on else "#888"
self.mic_btn.setIcon(icon_mic(color))
class MasterPreSection(QFrame):
def __init__(self, engine, parent=None):
super().__init__(parent)
self.engine = engine
self._master = engine.mixer['master']
self.setFixedWidth(80)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
self.setStyleSheet("""
MasterPreSection {
background-color: #101216;
border: 1px solid #1e2026;
border-radius: 6px;
}
""")
self._build_ui()
self.timer = QTimer(self)
self.timer.setInterval(30)
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)
header = QLabel("MASTER")
header.setFont(QFont("Segoe UI", 10, QFont.Bold))
header.setAlignment(Qt.AlignCenter)
header.setStyleSheet("color: #888; background: transparent; border: none;")
root.addWidget(header)
meter_row = QHBoxLayout()
meter_row.setSpacing(2)
self.vu_meter = VUMeter()
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);
border-radius: 2px;
}
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)
def _on_volume_changed(self, value: int):
vol = (value / 100.0) ** 2
self._master.volume = vol
def _refresh(self):
self.vu_meter.set_value(self._master.vu_peak)
self.volume_slider.setValue(int(math.sqrt(self._master.volume) * 100))
class EffectSection(QFrame):
def __init__(self, engine, parent=None):
super().__init__(parent)
self.engine = engine
self._master = engine.mixer['master']
self._knobs = {}
self.setFixedWidth(110)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
self.setStyleSheet("""
EffectSection {
background-color: #181a1e;
border: 1px solid #282a30;
border-radius: 6px;
}
""")
self._build_ui()
self.timer = QTimer(self)
self.timer.setInterval(30)
self.timer.timeout.connect(self._refresh)
self.timer.start()
def _build_ui(self):
root = QVBoxLayout(self)
root.setContentsMargins(3, 6, 3, 6)
root.setSpacing(2)
header = QLabel("FX")
header.setFont(QFont("Segoe UI", 10, QFont.Bold))
header.setAlignment(Qt.AlignCenter)
header.setStyleSheet("color: #556670; background: transparent; border: none;")
root.addWidget(header)
lufs_frame = QFrame()
lufs_frame.setStyleSheet("QFrame { background-color: #121318; border: 1px solid #22242a; border-radius: 4px; }")
lufs_layout = QVBoxLayout(lufs_frame)
lufs_layout.setContentsMargins(3, 4, 3, 4)
lufs_layout.setSpacing(2)
self.lufs_value = QLabel("-70.0")
self.lufs_value.setFont(QFont("Segoe UI", 14, QFont.Bold))
self.lufs_value.setAlignment(Qt.AlignCenter)
self.lufs_value.setStyleSheet("color: #00cc88; background: transparent; border: none;")
lufs_layout.addWidget(self.lufs_value)
lufs_unit = QLabel("LUFS")
lufs_unit.setFont(QFont("Segoe UI", 7))
lufs_unit.setAlignment(Qt.AlignCenter)
lufs_unit.setStyleSheet("color: #556; background: transparent; border: none;")
lufs_layout.addWidget(lufs_unit)
self.pfl_indicator = QLabel("PFL\n—")
self.pfl_indicator.setFont(QFont("Segoe UI", 7))
self.pfl_indicator.setAlignment(Qt.AlignCenter)
self.pfl_indicator.setStyleSheet("color: #556; background: transparent; border: none;")
lufs_layout.addWidget(self.pfl_indicator)
root.addWidget(lufs_frame)
knob_col = QVBoxLayout()
knob_col.setSpacing(1)
bypass_row = QHBoxLayout()
bypass_row.setSpacing(2)
for label, btn_ref, obj, attr in [
("C", "comp_bypass", self._master.compressor, 'bypassed'),
("L", "lim_bypass", self._master.limiter, 'bypassed'),
]:
btn = QPushButton(label)
btn.setFixedSize(24, 18)
btn.setCheckable(True)
btn.setFont(QFont("Segoe UI", 7, QFont.Bold))
btn.setStyleSheet("""
QPushButton { background-color: #22242a; color: #889; border: 1px solid #333; border-radius: 3px; }
QPushButton:checked { background-color: #4a1a1a; color: #ff6666; border: 1px solid #ff4444; }
""")
setattr(self, btn_ref, btn)
btn.toggled.connect(lambda c, o=obj, a=attr: setattr(o, a, c))
bypass_row.addWidget(btn)
knob_col.addLayout(bypass_row)
for label, attr, default, lo, hi, suffix in [
("THR", "threshold_db", -20, -60, 0, "dB"),
("RAT", "ratio", 4, 1, 20, ":1"),
("ATK", "attack_ms", 5, 0.1, 50, "ms"),
("REL", "release_ms", 100, 10, 1000, "ms"),
("MKG", "makeup_gain_db", 0, 0, 24, "dB"),
("CEIL", "ceiling_db", -0.1, -10, 0, "dB"),
]:
obj = self._master.compressor if attr != "ceiling_db" else self._master.limiter
k, dial, val_label = self._make_knob(label, obj, attr, default, lo, hi, suffix)
knob_col.addWidget(k)
self._knobs[('compressor' if attr != 'ceiling_db' else 'limiter', attr)] = (dial, val_label, lo, hi, suffix)
root.addLayout(knob_col)
def _make_knob(self, label, obj, attr, default, lo, hi, suffix):
frame = QFrame()
frame.setStyleSheet("background: transparent; border: none;")
layout = QHBoxLayout(frame)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(2)
dial = QDial()
dial.setFixedSize(30, 30)
dial.setRange(0, 1000)
dial.setNotchesVisible(False)
dial.setStyleSheet("""
QDial { background-color: #121318; border: 1px solid #2a2c33; border-radius: 15px; }
""")
def _val_to_pos(v):
return int((v - lo) / (hi - lo) * 1000)
def _pos_to_val(p):
return lo + (p / 1000.0) * (hi - lo)
dial.setValue(_val_to_pos(default))
lbl = QLabel(label)
lbl.setFont(QFont("Segoe UI", 9, QFont.Bold))
lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
lbl.setStyleSheet("color: #889; background: transparent; border: none;")
val_label = QLabel(f"{default}{suffix}")
val_label.setFont(QFont("Segoe UI", 8))
val_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
val_label.setStyleSheet("color: #667; background: transparent; border: none;")
def _on_dial(v):
val = _pos_to_val(v)
if attr == "ratio":
val = round(max(1, val), 1)
elif attr in ("threshold_db", "ceiling_db", "makeup_gain_db"):
val = round(max(lo, min(hi, val)), 1)
elif attr in ("attack_ms",):
val = round(max(0.1, val), 1)
elif attr == "release_ms":
val = round(max(1, val), 0)
setattr(obj, attr, val)
if attr == "attack_ms":
obj.set_attack(val)
elif attr == "release_ms":
obj.set_release(val)
val_label.setText(f"{val}{suffix}")
dial.valueChanged.connect(_on_dial)
layout.addWidget(dial)
layout.addWidget(lbl, 1)
layout.addWidget(val_label)
return frame, dial, val_label
def _refresh(self):
for (section, attr), (dial, val_label, lo, hi, suffix) in self._knobs.items():
obj = self._master.compressor if section == 'compressor' else self._master.limiter
val = getattr(obj, attr, lo)
dial.blockSignals(True)
dial.setValue(int((val - lo) / (hi - lo) * 1000))
dial.blockSignals(False)
val_label.setText(f"{val}{suffix}")
self.lufs_value.setText(f"{self._master.lufs_current:.1f}")
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:
self.pfl_indicator.setText("PFL\n—")
self.pfl_indicator.setStyleSheet("color: #556; background: transparent; border: none;")
class MixerWidget(QFrame):
def __init__(self, engine, parent=None):
super().__init__(parent)
self.engine = engine
self.setStyleSheet("""
MixerWidget {
background-color: #121318;
border: 1px solid #282a30;
border-radius: 8px;
}
""")
self._build_ui()
def _build_ui(self):
root = QVBoxLayout(self)
root.setContentsMargins(6, 4, 6, 6)
root.setSpacing(4)
header = QLabel("MIXER")
header.setFont(QFont("Segoe UI", 11, QFont.Bold))
header.setStyleSheet("color: #556670; background: transparent; border: none; "
"letter-spacing: 2px;")
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 {
background: #1a1c22;
height: 6px;
border-radius: 3px;
}
QScrollBar::handle:horizontal {
background: #3a3c44;
border-radius: 3px;
}
QScrollBar::handle:horizontal:hover { background: #5a5c64; }
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
width: 0px;
}
""")
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)
self.master_pre = MasterPreSection(self.engine)
channels_layout.addWidget(self.master_pre)
self.effects = EffectSection(self.engine)
channels_layout.addWidget(self.effects)
scroll.setWidget(scroll_content)
root.addWidget(scroll)
def apply_theme(self, theme: dict):
for strip in self._strips:
strip.apply_theme(theme)
self.master_pre.apply_theme(theme)
self.effects.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.pfl_btn.setChecked(ch.pfl)