v0.4 - cockybastard

This commit is contained in:
2026-05-13 20:53:15 +10:00
parent 7de0cc3de0
commit 6067f442b0
5 changed files with 174 additions and 87 deletions
+15 -15
View File
@@ -29,7 +29,7 @@ class CartSlot(QFrame):
} }
""") """)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setFixedHeight(110) self.setFixedHeight(80)
self._build_ui() self._build_ui()
self._connect_signals() self._connect_signals()
@@ -41,13 +41,13 @@ class CartSlot(QFrame):
def _build_ui(self): def _build_ui(self):
root = QHBoxLayout(self) root = QHBoxLayout(self)
root.setContentsMargins(8, 6, 8, 6) root.setContentsMargins(6, 4, 6, 4)
root.setSpacing(8) root.setSpacing(6)
badge = QLabel(str(self.slot_number)) badge = QLabel(str(self.slot_number))
badge.setFixedSize(28, 28) badge.setFixedSize(22, 22)
badge.setAlignment(Qt.AlignCenter) badge.setAlignment(Qt.AlignCenter)
badge.setFont(QFont("Arial", 12, QFont.Bold)) badge.setFont(QFont("Arial", 10, QFont.Bold))
badge.setStyleSheet(""" badge.setStyleSheet("""
background-color: #cc3300; background-color: #cc3300;
color: white; color: white;
@@ -67,11 +67,11 @@ class CartSlot(QFrame):
self.lcd_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.lcd_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
lcd_layout = QVBoxLayout(self.lcd_frame) lcd_layout = QVBoxLayout(self.lcd_frame)
lcd_layout.setContentsMargins(6, 4, 6, 4) lcd_layout.setContentsMargins(4, 2, 4, 2)
lcd_layout.setSpacing(2) lcd_layout.setSpacing(1)
self.track_label = QLabel("EMPTY") self.track_label = QLabel("EMPTY")
self.track_label.setFont(QFont("Courier New", 9, QFont.Bold)) self.track_label.setFont(QFont("Courier New", 8, QFont.Bold))
self.track_label.setStyleSheet( self.track_label.setStyleSheet(
"color: #00ff41; background: transparent; border: none;" "color: #00ff41; background: transparent; border: none;"
) )
@@ -80,13 +80,13 @@ class CartSlot(QFrame):
time_row = QHBoxLayout() time_row = QHBoxLayout()
self.elapsed_label = QLabel("00:00") self.elapsed_label = QLabel("00:00")
self.elapsed_label.setFont(QFont("Courier New", 14, QFont.Bold)) self.elapsed_label.setFont(QFont("Courier New", 11, QFont.Bold))
self.elapsed_label.setStyleSheet( self.elapsed_label.setStyleSheet(
"color: #00ff41; background: transparent; border: none;" "color: #00ff41; background: transparent; border: none;"
) )
self.remaining_label = QLabel("-00:00") self.remaining_label = QLabel("-00:00")
self.remaining_label.setFont(QFont("Courier New", 10)) self.remaining_label.setFont(QFont("Courier New", 8))
self.remaining_label.setStyleSheet( self.remaining_label.setStyleSheet(
"color: #00cc33; background: transparent; border: none;" "color: #00cc33; background: transparent; border: none;"
) )
@@ -109,11 +109,11 @@ class CartSlot(QFrame):
root.addWidget(self.lcd_frame) root.addWidget(self.lcd_frame)
btn_col = QVBoxLayout() btn_col = QVBoxLayout()
btn_col.setSpacing(4) btn_col.setSpacing(3)
self.btn_play = QPushButton() self.btn_play = QPushButton()
self.btn_play.setFixedSize(60, 44) self.btn_play.setFixedSize(50, 32)
self.btn_play.setIconSize(QSize(22, 22)) self.btn_play.setIconSize(QSize(18, 18))
self.btn_play.setIcon(icon_play("#00ff41")) self.btn_play.setIcon(icon_play("#00ff41"))
self.btn_play.setStyleSheet(""" self.btn_play.setStyleSheet("""
QPushButton { QPushButton {
@@ -126,8 +126,8 @@ class CartSlot(QFrame):
""") """)
self.btn_eject = QPushButton() self.btn_eject = QPushButton()
self.btn_eject.setFixedSize(60, 30) self.btn_eject.setFixedSize(50, 24)
self.btn_eject.setIconSize(QSize(18, 18)) self.btn_eject.setIconSize(QSize(14, 14))
self.btn_eject.setIcon(icon_eject("#ff4444")) self.btn_eject.setIcon(icon_eject("#ff4444"))
self.btn_eject.setStyleSheet(""" self.btn_eject.setStyleSheet("""
QPushButton { QPushButton {
+1 -1
View File
@@ -54,7 +54,7 @@ class RadioPanelWindow(QMainWindow):
self.deck4 = ExternalDeckWidget("deck4", "DECK 4", "#e17055", self.engine) self.deck4 = ExternalDeckWidget("deck4", "DECK 4", "#e17055", self.engine)
self.cart_bank = CartBankWidget(self.engine) self.cart_bank = CartBankWidget(self.engine)
self.cart_bank.setFixedHeight(220) self.cart_bank.setFixedHeight(180)
self.playlist_widget = PlaylistWidget( self.playlist_widget = PlaylistWidget(
engine=self.engine, engine=self.engine,
+61 -23
View File
@@ -18,6 +18,8 @@ logger = logging.getLogger(__name__)
NOTE_ON = 0x90 NOTE_ON = 0x90
NOTE_OFF = 0x80 NOTE_OFF = 0x80
CC = 0xB0 CC = 0xB0
PITCH_BEND = 0xE0
PITCH_BEND = 0xE0
CONFIG_DIR = os.path.expanduser("~/.gti_radiostudio") CONFIG_DIR = os.path.expanduser("~/.gti_radiostudio")
CONFIG_FILE = os.path.join(CONFIG_DIR, "midi_map.json") CONFIG_FILE = os.path.join(CONFIG_DIR, "midi_map.json")
@@ -40,12 +42,13 @@ class MidiMapping:
self.bindings = {} self.bindings = {}
def add(self, action: str, target_id, midi_type: int, midi_channel: int, def add(self, action: str, target_id, midi_type: int, midi_channel: int,
midi_note: int, inverse: bool = False): midi_note: int, inverse: bool = False, is_relative: bool = False):
key = (midi_type, midi_channel, midi_note) key = (midi_type, midi_channel, midi_note)
self.bindings[key] = { self.bindings[key] = {
'action': action, 'action': action,
'target_id': target_id, 'target_id': target_id,
'inverse': inverse, 'inverse': inverse,
'is_relative': is_relative,
} }
def lookup(self, midi_type: int, midi_channel: int, def lookup(self, midi_type: int, midi_channel: int,
@@ -63,6 +66,7 @@ class MidiMapping:
'action': val['action'], 'action': val['action'],
'target_id': val['target_id'], 'target_id': val['target_id'],
'inverse': val['inverse'], 'inverse': val['inverse'],
'is_relative': val.get('is_relative', False),
}) })
with open(filepath, 'w') as f: with open(filepath, 'w') as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
@@ -78,6 +82,7 @@ class MidiMapping:
'action': entry['action'], 'action': entry['action'],
'target_id': entry['target_id'], 'target_id': entry['target_id'],
'inverse': entry.get('inverse', False), 'inverse': entry.get('inverse', False),
'is_relative': entry.get('is_relative', False),
} }
@@ -93,6 +98,7 @@ class MidiEngine(QObject):
master_control = Signal(str, float) master_control = Signal(str, float)
controller_connected = Signal(bool, str) controller_connected = Signal(bool, str)
mapping_learned = Signal(str, object, int, int, int, bool) mapping_learned = Signal(str, object, int, int, int, bool)
raw_midi_event = Signal(str, int, int, int)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@@ -107,6 +113,9 @@ class MidiEngine(QObject):
self._learn_mode = False self._learn_mode = False
self._last_cc_values = {} self._last_cc_values = {}
self._channel_volumes = {}
self._master_knob_values = {}
self._master_knob_values = {}
@property @property
def learn_mode(self) -> bool: def learn_mode(self) -> bool:
@@ -200,13 +209,18 @@ class MidiEngine(QObject):
status_type = status & 0xF0 status_type = status & 0xF0
channel = status & 0x0F channel = status & 0x0F
# Learn mode: capture any MIDI event type_names = {0x90: "note_on", 0x80: "note_off", 0xB0: "cc", 0xE0: "pitch", 0xD0: "aftertouch"}
type_name = type_names.get(status_type, f"0x{status_type:02X}")
self.raw_midi_event.emit(type_name, channel, data_byte, value)
if self._learn_mode and self._learn_target: if self._learn_mode and self._learn_target:
action, target_id = self._learn_target action, target_id = self._learn_target
mapping_type = status_type mapping_type = status_type
self.mapping.add(action, target_id, mapping_type, channel, data_byte) is_rel = (status_type == 0xB0 and (1 <= value <= 63 or 65 <= value <= 127))
note = 0 if status_type == PITCH_BEND else data_byte
self.mapping.add(action, target_id, mapping_type, channel, note, is_relative=is_rel)
self.mapping.save() self.mapping.save()
self.mapping_learned.emit(action, target_id, mapping_type, channel, data_byte, value > 0) self.mapping_learned.emit(action, target_id, mapping_type, channel, note, value > 0)
self.stop_learn() self.stop_learn()
return return
@@ -218,6 +232,11 @@ class MidiEngine(QObject):
elif status_type == CC: elif status_type == CC:
self._handle_cc(data_byte, value) self._handle_cc(data_byte, value)
self._handle_learned_event(status_type, channel, data_byte, value) self._handle_learned_event(status_type, channel, data_byte, value)
elif status_type == PITCH_BEND:
lsb = data_byte
msb = message[2] if len(message) > 2 else 0
pb_value = (msb << 7) | lsb
self._handle_learned_event(status_type, channel, 0, pb_value)
def _handle_learned_event(self, midi_type: int, channel: int, def _handle_learned_event(self, midi_type: int, channel: int,
note: int, value: int): note: int, value: int):
@@ -228,6 +247,7 @@ class MidiEngine(QObject):
action = binding['action'] action = binding['action']
target_id = binding['target_id'] target_id = binding['target_id']
inv = binding.get('inverse', False) inv = binding.get('inverse', False)
is_rel = binding.get('is_relative', False)
if action == 'deck_play_pause' and isinstance(target_id, str): if action == 'deck_play_pause' and isinstance(target_id, str):
self.deck_play_pause.emit(target_id) self.deck_play_pause.emit(target_id)
@@ -235,15 +255,25 @@ class MidiEngine(QObject):
self.deck_cue.emit(target_id) self.deck_cue.emit(target_id)
elif action == 'cart_trigger': elif action == 'cart_trigger':
self.cart_trigger.emit(target_id) self.cart_trigger.emit(target_id)
elif action == 'deck_play_pause' and isinstance(target_id, str):
self.deck_play_pause.emit(target_id)
elif action == 'deck_cue' and isinstance(target_id, str):
self.deck_cue.emit(target_id)
elif action == 'cart_trigger':
self.cart_trigger.emit(target_id)
elif action == 'mixer_volume': elif action == 'mixer_volume':
scaled = (127 - value if inv else value) / 127.0 if is_rel and 1 <= value <= 63:
self.mixer_volume.emit(target_id, scaled) prev = self._channel_volumes.get(target_id, 1.0)
new_vol = min(1.0, prev + value * 0.02)
self._channel_volumes[target_id] = new_vol
self.mixer_volume.emit(target_id, new_vol)
elif is_rel and 65 <= value <= 127:
prev = self._channel_volumes.get(target_id, 1.0)
steps = value & 0x3F
new_vol = max(0.0, prev - steps * 0.02)
self._channel_volumes[target_id] = new_vol
self.mixer_volume.emit(target_id, new_vol)
else:
if midi_type == PITCH_BEND:
scaled = (16383 - value if inv else value) / 16383.0
else:
scaled = (127 - value if inv else value) / 127.0
self._channel_volumes[target_id] = scaled
self.mixer_volume.emit(target_id, scaled)
elif action == 'mixer_mute': elif action == 'mixer_mute':
muted = (value < 64) if inv else (value > 63) muted = (value < 64) if inv else (value > 63)
self.mixer_mute.emit(target_id, muted) self.mixer_mute.emit(target_id, muted)
@@ -254,17 +284,25 @@ class MidiEngine(QObject):
on = (value > 63) if not inv else (value < 64) on = (value > 63) if not inv else (value < 64)
self.mixer_pfl.emit(target_id, on) self.mixer_pfl.emit(target_id, on)
elif action == 'master_control': elif action == 'master_control':
scaled = (127 - value if inv else value) / 127.0 knob = str(target_id)
self.master_control.emit(str(target_id), scaled) if is_rel and 1 <= value <= 63:
elif action == 'mixer_solo': prev = self._master_knob_values.get(knob, 0.5)
on = (value > 63) if not inv else (value < 64) new_val = min(1.0, prev + value * 0.02)
self.mixer_solo.emit(target_id, on) self._master_knob_values[knob] = new_val
elif action == 'mixer_pfl': self.master_control.emit(knob, new_val)
on = (value > 63) if not inv else (value < 64) elif is_rel and 65 <= value <= 127:
self.mixer_pfl.emit(target_id, on) prev = self._master_knob_values.get(knob, 0.5)
elif action == 'master_control': steps = value & 0x3F
scaled = (127 - value if inv else value) / 127.0 new_val = max(0.0, prev - steps * 0.02)
self.master_control.emit(str(target_id), scaled) self._master_knob_values[knob] = new_val
self.master_control.emit(knob, new_val)
else:
if midi_type == PITCH_BEND:
scaled = (16383 - value if inv else value) / 16383.0
else:
scaled = (127 - value if inv else value) / 127.0
self._master_knob_values[knob] = scaled
self.master_control.emit(knob, scaled)
def _handle_note_on(self, note: int): def _handle_note_on(self, note: int):
# Hardcoded Numark DJ2Go mapping (legacy) # Hardcoded Numark DJ2Go mapping (legacy)
+60 -21
View File
@@ -10,6 +10,7 @@ from PySide6.QtGui import QFont, QPainter, QColor, QLinearGradient, QPen
from audio_engine import NUM_MIXER_CHANNELS from audio_engine import NUM_MIXER_CHANNELS
from icons import icon_mute, icon_solo from icons import icon_mute, icon_solo
import math
class VUMeter(QWidget): class VUMeter(QWidget):
@@ -27,23 +28,29 @@ class VUMeter(QWidget):
painter = QPainter(self) painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
rect = self.rect().adjusted(14, 3, -3, -3) rect = self.rect().adjusted(18, 3, -3, -3)
if rect.width() < 1 or rect.height() < 1: if rect.width() < 1 or rect.height() < 1:
painter.end() painter.end()
return return
painter.fillRect(rect, QColor("#080808")) painter.fillRect(rect, QColor("#080808"))
db_markings = [(-40, "#1a1a1a"), (-30, "#1a1a1a"), (-20, "#1a1a1a"), db_markings = [(-40,), (-30,), (-20,), (-12,), (-6,), (0,)]
(-12, "#2a2a1a"), (-6, "#2a1a1a"), (0, "#2a0a0a")] for (db,) in db_markings:
for db, color in db_markings:
pct = (db + 46) / 46.0 if db <= 0 else 1.0 pct = (db + 46) / 46.0 if db <= 0 else 1.0
pct = max(0.0, min(1.0, pct)) pct = max(0.0, min(1.0, pct))
y = rect.bottom() - int(rect.height() * pct) y = rect.bottom() - int(rect.height() * pct)
painter.setPen(QPen(QColor(color), 1)) if db == -6:
painter.drawLine(rect.right() - 5, y, rect.right(), y) painter.setPen(QPen(QColor("#00ff88"), 2))
painter.setPen(QPen(QColor("#444444"), 1)) painter.drawLine(rect.right() - 3, y, rect.right(), y)
painter.drawText(rect.left() - 13, y + 4, f"{db}") painter.setFont(QFont("Segoe UI", 8, 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", 7))
painter.drawText(rect.left() - 16, y + 3, f"{db}")
painter.setPen(QPen(QColor("#1a1a1a"), 1)) painter.setPen(QPen(QColor("#1a1a1a"), 1))
painter.drawRect(rect) painter.drawRect(rect)
@@ -63,12 +70,13 @@ class VUMeter(QWidget):
db = -46 * (1 - val) if val < 1.0 else 3 db = -46 * (1 - val) if val < 1.0 else 3
fill_pct = (db + 46) / 49.0 if db <= 0 else 1.0 fill_pct = (db + 46) / 49.0 if db <= 0 else 1.0
fill_pct = max(0.0, min(1.0, fill_pct)) fill_pct = max(0.0, min(1.0, fill_pct))
fill_pct = math.sqrt(fill_pct)
fill_h = int(rect.height() * fill_pct) fill_h = int(rect.height() * fill_pct)
painter.fillRect(rect.x(), rect.bottom() - fill_h, painter.fillRect(rect.x(), rect.bottom() - fill_h,
rect.width(), fill_h, gradient) rect.width(), fill_h, gradient)
for db, _ in db_markings: for (db,) in db_markings:
pct = (db + 46) / 46.0 if db <= 0 else 1.0 pct = (db + 46) / 46.0 if db <= 0 else 1.0
pct = max(0.0, min(1.0, pct)) pct = max(0.0, min(1.0, pct))
y = rect.bottom() - int(rect.height() * pct) y = rect.bottom() - int(rect.height() * pct)
@@ -99,7 +107,7 @@ class ChannelStrip(QFrame):
self._connect_signals() self._connect_signals()
self.timer = QTimer(self) self.timer = QTimer(self)
self.timer.setInterval(80) self.timer.setInterval(30)
self.timer.timeout.connect(self._refresh) self.timer.timeout.connect(self._refresh)
self.timer.start() self.timer.start()
@@ -126,13 +134,15 @@ class ChannelStrip(QFrame):
""") """)
root.addWidget(self.name_btn) root.addWidget(self.name_btn)
meter_row = QHBoxLayout()
meter_row.setSpacing(2)
self.vu_meter = VUMeter() self.vu_meter = VUMeter()
root.addWidget(self.vu_meter, stretch=1) meter_row.addWidget(self.vu_meter, stretch=1)
self.volume_slider = QSlider(Qt.Vertical) self.volume_slider = QSlider(Qt.Vertical)
self.volume_slider.setRange(0, 100) self.volume_slider.setRange(0, 100)
self.volume_slider.setValue(100) self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
self.volume_slider.setFixedHeight(60)
self.volume_slider.setStyleSheet(""" self.volume_slider.setStyleSheet("""
QSlider::groove:vertical { QSlider::groove:vertical {
background: #22242a; background: #22242a;
@@ -147,13 +157,19 @@ class ChannelStrip(QFrame):
border-radius: 2px; border-radius: 2px;
} }
QSlider::handle:vertical:hover { background: #b0b8c8; } QSlider::handle:vertical:hover { background: #b0b8c8; }
QSlider::sub-page:vertical { QSlider::add-page:vertical {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #00cc66, stop:1 #008844); stop:0 #00cc66, stop:1 #008844);
border-radius: 2px; border-radius: 2px;
} }
QSlider::sub-page:vertical {
background: transparent;
border-radius: 2px;
}
""") """)
root.addWidget(self.volume_slider, alignment=Qt.AlignCenter) meter_row.addWidget(self.volume_slider, stretch=1)
root.addLayout(meter_row, stretch=1)
btn_row = QHBoxLayout() btn_row = QHBoxLayout()
btn_row.setSpacing(3) btn_row.setSpacing(3)
@@ -238,7 +254,8 @@ class ChannelStrip(QFrame):
self.name_btn.setText(self._channel.name) self.name_btn.setText(self._channel.name)
def _on_volume_changed(self, value: int): def _on_volume_changed(self, value: int):
self.engine.set_channel_volume(self.channel_index, value / 100.0) vol = (value / 100.0) ** 2
self.engine.set_channel_volume(self.channel_index, vol)
def _on_mute_toggled(self, checked: bool): def _on_mute_toggled(self, checked: bool):
self.engine.set_channel_mute(self.channel_index, checked) self.engine.set_channel_mute(self.channel_index, checked)
@@ -251,6 +268,8 @@ class ChannelStrip(QFrame):
def _refresh(self): def _refresh(self):
self.vu_meter.set_value(self._channel.vu_peak) self.vu_meter.set_value(self._channel.vu_peak)
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
def apply_theme(self, theme: dict): def apply_theme(self, theme: dict):
pass pass
@@ -262,8 +281,10 @@ class MasterSection(QFrame):
self.engine = engine self.engine = engine
self._master = engine.mixer['master'] self._master = engine.mixer['master']
self._knobs = {}
self.setMinimumWidth(180) self.setMinimumWidth(180)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setStyleSheet(""" self.setStyleSheet("""
MasterSection { MasterSection {
background-color: #181a1e; background-color: #181a1e;
@@ -276,7 +297,7 @@ class MasterSection(QFrame):
self._connect_signals() self._connect_signals()
self.timer = QTimer(self) self.timer = QTimer(self)
self.timer.setInterval(80) self.timer.setInterval(30)
self.timer.timeout.connect(self._refresh) self.timer.timeout.connect(self._refresh)
self.timer.start() self.timer.start()
@@ -355,9 +376,10 @@ class MasterSection(QFrame):
("REL", "release_ms", 100, 10, 1000, "ms"), ("REL", "release_ms", 100, 10, 1000, "ms"),
("MKG", "makeup_gain_db", 0, 0, 24, "dB"), ("MKG", "makeup_gain_db", 0, 0, 24, "dB"),
]: ]:
k = self._make_knob(label, self._master.compressor, attr, k, dial, val_label = self._make_knob(label, self._master.compressor, attr,
default, lo, hi, suffix) default, lo, hi, suffix)
knobs.addWidget(k) knobs.addWidget(k)
self._knobs[('compressor', attr)] = (dial, val_label, lo, hi, suffix)
comp_layout.addLayout(knobs) comp_layout.addLayout(knobs)
root.addWidget(comp_frame) root.addWidget(comp_frame)
@@ -402,9 +424,10 @@ class MasterSection(QFrame):
lim_knobs = QHBoxLayout() lim_knobs = QHBoxLayout()
lim_knobs.setSpacing(4) lim_knobs.setSpacing(4)
k = self._make_knob("CEIL", self._master.limiter, "ceiling_db", k, dial, val_label = self._make_knob("CEIL", self._master.limiter, "ceiling_db",
-0.1, -10, 0, "dB") -0.1, -10, 0, "dB")
lim_knobs.addWidget(k) lim_knobs.addWidget(k)
self._knobs[('limiter', 'ceiling_db')] = (dial, val_label, -10, 0, "dB")
lim_layout.addLayout(lim_knobs) lim_layout.addLayout(lim_knobs)
root.addWidget(lim_frame) root.addWidget(lim_frame)
root.addStretch() root.addStretch()
@@ -467,7 +490,7 @@ class MasterSection(QFrame):
dial.valueChanged.connect(_on_dial) dial.valueChanged.connect(_on_dial)
layout.addWidget(dial, alignment=Qt.AlignCenter) layout.addWidget(dial, alignment=Qt.AlignCenter)
layout.addWidget(val_label) layout.addWidget(val_label)
return frame return frame, dial, val_label
def _connect_signals(self): def _connect_signals(self):
self.comp_bypass.toggled.connect( self.comp_bypass.toggled.connect(
@@ -493,6 +516,22 @@ class MasterSection(QFrame):
"color: #556; background: transparent; border: none;" "color: #556; background: transparent; border: none;"
) )
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}")
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}")
def apply_theme(self, theme: dict): def apply_theme(self, theme: dict):
pass pass
+37 -27
View File
@@ -2,7 +2,7 @@
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QFrame, QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QFrame, QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
QComboBox, QScrollArea, QWidget QComboBox, QScrollArea, QWidget, QListWidget
) )
from PySide6.QtCore import Qt, QTimer, Signal, QSize, QPoint from PySide6.QtCore import Qt, QTimer, Signal, QSize, QPoint
from PySide6.QtGui import QFont from PySide6.QtGui import QFont
@@ -134,7 +134,6 @@ class TopBarWidget(QFrame):
def _toggle_settings_popup(self): def _toggle_settings_popup(self):
self._popup = SettingsPopup(self._midi_engine, self) self._popup = SettingsPopup(self._midi_engine, self)
self._popup.ratio_changed.connect(self.ratio_changed.emit)
self._popup.save_session.connect(self.save_session.emit) self._popup.save_session.connect(self.save_session.emit)
self._popup.load_session.connect(self.load_session.emit) self._popup.load_session.connect(self.load_session.emit)
pos = self.settings_btn.mapToGlobal(QPoint(0, self.settings_btn.height() + 4)) pos = self.settings_btn.mapToGlobal(QPoint(0, self.settings_btn.height() + 4))
@@ -244,14 +243,12 @@ POPUP_STYLE = """
class SettingsPopup(QFrame): class SettingsPopup(QFrame):
ratio_changed = Signal(int, int)
save_session = Signal() save_session = Signal()
load_session = Signal() load_session = Signal()
def __init__(self, midi_engine, parent=None): def __init__(self, midi_engine, parent=None):
super().__init__(parent) super().__init__(parent)
self._midi_engine = midi_engine self._midi_engine = midi_engine
self._ratio_index = 1
self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
self.setObjectName("settingsPopup") self.setObjectName("settingsPopup")
@@ -265,21 +262,9 @@ class SettingsPopup(QFrame):
root.setContentsMargins(12, 10, 12, 10) root.setContentsMargins(12, 10, 12, 10)
root.setSpacing(6) root.setSpacing(6)
# Row 1: Aspect ratio + Save/Load # Row 1: Save/Load
row1 = QHBoxLayout() row1 = QHBoxLayout()
row1.setSpacing(6) row1.setSpacing(6)
self._ratio_btns = []
for i, (label, w, h) in enumerate(RATIOS):
btn = QPushButton(label)
btn.setObjectName("ratioBtn")
btn.setFixedHeight(24)
btn.setCheckable(True)
btn.setChecked(i == self._ratio_index)
btn.clicked.connect(partial(self._set_ratio, i))
row1.addWidget(btn)
self._ratio_btns.append(btn)
row1.addSpacing(8)
save_btn = QPushButton("SAVE") save_btn = QPushButton("SAVE")
save_btn.setObjectName("popupBtn") save_btn.setObjectName("popupBtn")
save_btn.setFixedHeight(24) save_btn.setFixedHeight(24)
@@ -326,7 +311,7 @@ class SettingsPopup(QFrame):
scroll = QScrollArea() scroll = QScrollArea()
scroll.setWidgetResizable(True) scroll.setWidgetResizable(True)
scroll.setFixedHeight(170) scroll.setFixedHeight(200)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setStyleSheet(POPUP_STYLE) scroll.setStyleSheet(POPUP_STYLE)
@@ -347,6 +332,14 @@ class SettingsPopup(QFrame):
("Cart 2", "cart_trigger", 1), ("Cart 2", "cart_trigger", 1),
("Cart 3", "cart_trigger", 2), ("Cart 3", "cart_trigger", 2),
("Cart 4", "cart_trigger", 3), ("Cart 4", "cart_trigger", 3),
("Ch 1 Volume", "mixer_volume", 0),
("Ch 2 Volume", "mixer_volume", 1),
("Ch 3 Volume", "mixer_volume", 2),
("Ch 4 Volume", "mixer_volume", 3),
("Ch 5 Volume", "mixer_volume", 4),
("Ch 6 Volume", "mixer_volume", 5),
("Ch 7 Volume", "mixer_volume", 6),
("Ch 8 Volume", "mixer_volume", 7),
("Ch 1 Solo", "mixer_solo", 0), ("Ch 1 Solo", "mixer_solo", 0),
("Ch 1 PFL", "mixer_pfl", 0), ("Ch 1 PFL", "mixer_pfl", 0),
("Ch 2 Solo", "mixer_solo", 1), ("Ch 2 Solo", "mixer_solo", 1),
@@ -392,7 +385,7 @@ class SettingsPopup(QFrame):
font-weight: bold; font-weight: bold;
padding: 2px 4px; padding: 2px 4px;
} }
QPushButton:hover { background-color: #5a2a2a; } QPushButton:hover { background-color: #5a2a2a; }
""") """)
clear_btn.setFixedSize(20, 20) clear_btn.setFixedSize(20, 20)
clear_btn.clicked.connect(partial(self._clear_mapping, action, target_id)) clear_btn.clicked.connect(partial(self._clear_mapping, action, target_id))
@@ -403,19 +396,36 @@ class SettingsPopup(QFrame):
scroll.setWidget(content) scroll.setWidget(content)
root.addWidget(scroll) root.addWidget(scroll)
monitor_label = QLabel("MIDI MONITOR")
monitor_label.setStyleSheet("color: #556670; background: transparent; border: none; font-size: 7pt; font-weight: bold;")
root.addWidget(monitor_label)
self._midi_monitor = QListWidget()
self._midi_monitor.setFixedHeight(80)
self._midi_monitor.setStyleSheet("""
QListWidget {
background-color: #0d0e12;
border: 1px solid #22242a;
border-radius: 3px;
color: #8890a0;
font-size: 7pt;
font-family: monospace;
}
""")
self._refresh_ports() self._refresh_ports()
if self._midi_engine: if self._midi_engine:
self._midi_engine.mapping_learned.connect(self._on_mapping_learned) self._midi_engine.mapping_learned.connect(self._on_mapping_learned)
self._midi_engine.raw_midi_event.connect(self._on_raw_midi)
# ── Ratio ────────────────────────────────────────────────────── def _on_raw_midi(self, type_name, channel, control, value):
self._midi_monitor.addItem(
def _set_ratio(self, index): f"{type_name:>10} ch{channel} ctl={control:>3} val={value:>3}"
self._ratio_index = index )
for i, btn in enumerate(self._ratio_btns): while self._midi_monitor.count() > 200:
btn.setChecked(i == index) self._midi_monitor.takeItem(0)
_, w, h = RATIOS[index] self._midi_monitor.scrollToBottom()
self.ratio_changed.emit(w, h)
# ── MIDI port ────────────────────────────────────────────────── # ── MIDI port ──────────────────────────────────────────────────