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:
+72
-15
@@ -143,6 +143,14 @@ class DeckState:
|
|||||||
return f"-{int(remaining//60):02d}:{int(remaining%60):02d}"
|
return f"-{int(remaining//60):02d}:{int(remaining%60):02d}"
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalInputMonitor:
|
||||||
|
def __init__(self, buffer_size=600):
|
||||||
|
self.buffer = np.zeros(buffer_size, dtype=np.float32)
|
||||||
|
self.write_pos = 0
|
||||||
|
self.buffer_size = buffer_size
|
||||||
|
self.monitoring = False
|
||||||
|
|
||||||
|
|
||||||
class MixerChannel:
|
class MixerChannel:
|
||||||
def __init__(self, name: str, index: int):
|
def __init__(self, name: str, index: int):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -150,14 +158,10 @@ class MixerChannel:
|
|||||||
self.volume = 1.0
|
self.volume = 1.0
|
||||||
self.muted = False
|
self.muted = False
|
||||||
self.solo = False
|
self.solo = False
|
||||||
|
self.pfl = False
|
||||||
self.vu_peak = 0.0
|
self.vu_peak = 0.0
|
||||||
self.in_port_L = None
|
self.in_port_L = None
|
||||||
self.in_port_R = None
|
self.in_port_R = None
|
||||||
self.out_port_L = None
|
|
||||||
self.out_port_R = None
|
|
||||||
|
|
||||||
def reset_vu(self):
|
|
||||||
self.vu_peak = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class Compressor:
|
class Compressor:
|
||||||
@@ -169,7 +173,6 @@ class Compressor:
|
|||||||
self.release_ms = 100.0
|
self.release_ms = 100.0
|
||||||
self.makeup_gain_db = 0.0
|
self.makeup_gain_db = 0.0
|
||||||
self.bypassed = False
|
self.bypassed = False
|
||||||
|
|
||||||
self._envelope = 0.0
|
self._envelope = 0.0
|
||||||
self._attack_coeff = self._time_constant(self.attack_ms)
|
self._attack_coeff = self._time_constant(self.attack_ms)
|
||||||
self._release_coeff = self._time_constant(self.release_ms)
|
self._release_coeff = self._time_constant(self.release_ms)
|
||||||
@@ -226,16 +229,13 @@ class MasterBus:
|
|||||||
self.vu_peak = 0.0
|
self.vu_peak = 0.0
|
||||||
self.lufs_current = -70.0
|
self.lufs_current = -70.0
|
||||||
|
|
||||||
def reset_vu(self):
|
|
||||||
self.vu_peak = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class AudioEngine:
|
class AudioEngine:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.client = jack.Client("RadioPanel")
|
self.client = jack.Client("RadioPanel")
|
||||||
self.sr = self.client.samplerate
|
self.sr = self.client.samplerate
|
||||||
|
|
||||||
# Output ports for decks (standalone, not mixed into master)
|
# Output ports for digital decks (standalone, not mixed into master)
|
||||||
self.ports = {
|
self.ports = {
|
||||||
'deck1': (
|
'deck1': (
|
||||||
self.client.outports.register("deck1_L"),
|
self.client.outports.register("deck1_L"),
|
||||||
@@ -251,6 +251,17 @@ class AudioEngine:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# External input monitors (deck3, deck4 — pass-through with waveform)
|
||||||
|
self.ext_monitors = {}
|
||||||
|
self.ext_ports = {}
|
||||||
|
for ext_key, label in [('deck3', 'deck3'), ('deck4', 'deck4')]:
|
||||||
|
in_L = self.client.inports.register(f"{label}_in_L")
|
||||||
|
in_R = self.client.inports.register(f"{label}_in_R")
|
||||||
|
out_L = self.client.outports.register(f"{label}_out_L")
|
||||||
|
out_R = self.client.outports.register(f"{label}_out_R")
|
||||||
|
self.ext_ports[ext_key] = (in_L, in_R, out_L, out_R)
|
||||||
|
self.ext_monitors[ext_key] = ExternalInputMonitor()
|
||||||
|
|
||||||
# Mixer channels: 8 stereo input ports
|
# Mixer channels: 8 stereo input ports
|
||||||
self.mixer = {
|
self.mixer = {
|
||||||
'channels': [],
|
'channels': [],
|
||||||
@@ -268,7 +279,13 @@ class AudioEngine:
|
|||||||
self.client.outports.register("master_out_R"),
|
self.client.outports.register("master_out_R"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Deck states (unchanged)
|
# PFL output ports
|
||||||
|
self.pfl_out = (
|
||||||
|
self.client.outports.register("pfl_out_L"),
|
||||||
|
self.client.outports.register("pfl_out_R"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deck states (for digital decks and carts)
|
||||||
self.decks = {
|
self.decks = {
|
||||||
'deck1': DeckState(),
|
'deck1': DeckState(),
|
||||||
'deck2': DeckState(),
|
'deck2': DeckState(),
|
||||||
@@ -300,6 +317,10 @@ class AudioEngine:
|
|||||||
if 0 <= index < NUM_MIXER_CHANNELS:
|
if 0 <= index < NUM_MIXER_CHANNELS:
|
||||||
self.mixer['channels'][index].solo = solo
|
self.mixer['channels'][index].solo = solo
|
||||||
|
|
||||||
|
def set_channel_pfl(self, index: int, pfl: bool):
|
||||||
|
if 0 <= index < NUM_MIXER_CHANNELS:
|
||||||
|
self.mixer['channels'][index].pfl = pfl
|
||||||
|
|
||||||
def _fill_port(self, port_L, port_R, deck: DeckState, frames: int):
|
def _fill_port(self, port_L, port_R, deck: DeckState, frames: int):
|
||||||
buf_L = port_L.get_array()
|
buf_L = port_L.get_array()
|
||||||
buf_R = port_R.get_array()
|
buf_R = port_R.get_array()
|
||||||
@@ -327,7 +348,7 @@ class AudioEngine:
|
|||||||
deck.position = track.num_samples - 1
|
deck.position = track.num_samples - 1
|
||||||
|
|
||||||
def _process(self, frames: int):
|
def _process(self, frames: int):
|
||||||
# ── Decks (unchanged) ──────────────────────────────────────────
|
# ── Digital decks ───────────────────────────────────────────────
|
||||||
self._fill_port(*self.ports['deck1'], self.decks['deck1'], frames)
|
self._fill_port(*self.ports['deck1'], self.decks['deck1'], frames)
|
||||||
self._fill_port(*self.ports['deck2'], self.decks['deck2'], frames)
|
self._fill_port(*self.ports['deck2'], self.decks['deck2'], frames)
|
||||||
|
|
||||||
@@ -355,6 +376,34 @@ class AudioEngine:
|
|||||||
deck.playing = False
|
deck.playing = False
|
||||||
deck.position = track.num_samples - 1
|
deck.position = track.num_samples - 1
|
||||||
|
|
||||||
|
# ── External input monitors (deck3, deck4) ─────────────────────
|
||||||
|
gate_linear = 10 ** (-46.0 / 20.0)
|
||||||
|
for ext_key in ('deck3', 'deck4'):
|
||||||
|
in_L, in_R, out_L, out_R = self.ext_ports[ext_key]
|
||||||
|
in_buf_L = in_L.get_array()
|
||||||
|
in_buf_R = in_R.get_array()
|
||||||
|
out_buf_L = out_L.get_array()
|
||||||
|
out_buf_R = out_R.get_array()
|
||||||
|
|
||||||
|
out_buf_L[:] = in_buf_L[:]
|
||||||
|
out_buf_R[:] = in_buf_R[:]
|
||||||
|
|
||||||
|
monitor = self.ext_monitors[ext_key]
|
||||||
|
if not monitor.monitoring:
|
||||||
|
monitor.buffer[:] = 0.0
|
||||||
|
continue
|
||||||
|
|
||||||
|
rms = np.sqrt(np.mean((in_buf_L ** 2 + in_buf_R ** 2) / 2.0))
|
||||||
|
if rms > gate_linear:
|
||||||
|
db = 20 * np.log10(max(rms, 1e-10))
|
||||||
|
display = max(0.0, (db + 46.0) / 46.0)
|
||||||
|
display = min(1.0, display)
|
||||||
|
else:
|
||||||
|
display = 0.0
|
||||||
|
|
||||||
|
monitor.buffer[monitor.write_pos] = display
|
||||||
|
monitor.write_pos = (monitor.write_pos + 1) % monitor.buffer_size
|
||||||
|
|
||||||
# ── Mixer ───────────────────────────────────────────────────────
|
# ── Mixer ───────────────────────────────────────────────────────
|
||||||
master_buf_L = self.master_out[0].get_array()
|
master_buf_L = self.master_out[0].get_array()
|
||||||
master_buf_R = self.master_out[1].get_array()
|
master_buf_R = self.master_out[1].get_array()
|
||||||
@@ -363,12 +412,23 @@ class AudioEngine:
|
|||||||
|
|
||||||
any_solo = any(ch.solo for ch in self.mixer['channels'])
|
any_solo = any(ch.solo for ch in self.mixer['channels'])
|
||||||
|
|
||||||
|
# PFL bus
|
||||||
|
pfl_buf_L = self.pfl_out[0].get_array()
|
||||||
|
pfl_buf_R = self.pfl_out[1].get_array()
|
||||||
|
pfl_buf_L.fill(0.0)
|
||||||
|
pfl_buf_R.fill(0.0)
|
||||||
|
|
||||||
for ch in self.mixer['channels']:
|
for ch in self.mixer['channels']:
|
||||||
in_L = ch.in_port_L.get_array()
|
in_L = ch.in_port_L.get_array()
|
||||||
in_R = ch.in_port_R.get_array()
|
in_R = ch.in_port_R.get_array()
|
||||||
|
|
||||||
mute = ch.muted or (any_solo and not ch.solo)
|
mute = ch.muted or (any_solo and not ch.solo)
|
||||||
|
|
||||||
|
# PFL: pre-fader, pre-mute signal to PFL bus
|
||||||
|
if ch.pfl:
|
||||||
|
pfl_buf_L[:] += in_L[:]
|
||||||
|
pfl_buf_R[:] += in_R[:]
|
||||||
|
|
||||||
if mute:
|
if mute:
|
||||||
ch.vu_peak = 0.0
|
ch.vu_peak = 0.0
|
||||||
continue
|
continue
|
||||||
@@ -390,15 +450,12 @@ class AudioEngine:
|
|||||||
self.mixer['master'].compressor.process(master_audio)
|
self.mixer['master'].compressor.process(master_audio)
|
||||||
self.mixer['master'].limiter.process(master_audio)
|
self.mixer['master'].limiter.process(master_audio)
|
||||||
|
|
||||||
# Copy processed audio back to port buffers
|
|
||||||
master_buf_L[:] = master_audio[0]
|
master_buf_L[:] = master_audio[0]
|
||||||
master_buf_R[:] = master_audio[1]
|
master_buf_R[:] = master_audio[1]
|
||||||
|
|
||||||
# Master VU
|
|
||||||
master_peak = max(np.max(np.abs(master_audio[0])), np.max(np.abs(master_audio[1])))
|
master_peak = max(np.max(np.abs(master_audio[0])), np.max(np.abs(master_audio[1])))
|
||||||
self.mixer['master'].vu_peak = master_peak
|
self.mixer['master'].vu_peak = master_peak
|
||||||
|
|
||||||
# LUFS calculation (every ~512 frames)
|
|
||||||
if frames % 512 < frames:
|
if frames % 512 < frames:
|
||||||
self._calculate_lufs(master_audio)
|
self._calculate_lufs(master_audio)
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -18,7 +18,6 @@ class CartSlot(QFrame):
|
|||||||
self.slot_number = slot_number
|
self.slot_number = slot_number
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
self._playing_icon = False
|
self._playing_icon = False
|
||||||
self._playing_icon = False
|
|
||||||
|
|
||||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
||||||
self.setLineWidth(1)
|
self.setLineWidth(1)
|
||||||
@@ -146,7 +145,6 @@ class CartSlot(QFrame):
|
|||||||
root.addLayout(btn_col)
|
root.addLayout(btn_col)
|
||||||
|
|
||||||
def apply_theme(self, theme: dict):
|
def apply_theme(self, theme: dict):
|
||||||
"""Apply theme to cart slot LCD."""
|
|
||||||
self.lcd_frame.setStyleSheet(f"""
|
self.lcd_frame.setStyleSheet(f"""
|
||||||
QFrame {{
|
QFrame {{
|
||||||
background-color: {theme['bg']};
|
background-color: {theme['bg']};
|
||||||
@@ -291,7 +289,5 @@ class CartBankWidget(QWidget):
|
|||||||
root.addLayout(grid)
|
root.addLayout(grid)
|
||||||
|
|
||||||
def apply_theme(self, theme: dict):
|
def apply_theme(self, theme: dict):
|
||||||
"""Propagate theme to all cart slots."""
|
|
||||||
for slot in self.slots:
|
for slot in self.slots:
|
||||||
slot.apply_theme(theme)
|
slot.apply_theme(theme)
|
||||||
|
|
||||||
+206
-69
@@ -5,7 +5,7 @@ from PySide6.QtWidgets import (
|
|||||||
QPushButton, QFrame, QSizePolicy
|
QPushButton, QFrame, QSizePolicy
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QTimer, Signal, QSize
|
from PySide6.QtCore import Qt, QTimer, Signal, QSize
|
||||||
from PySide6.QtGui import QFont, QColor, QPainter, QPen
|
from PySide6.QtGui import QFont, QColor, QPainter, QPen, QPainterPath
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -16,8 +16,6 @@ from icons import (
|
|||||||
|
|
||||||
|
|
||||||
class WaveformWidget(QFrame):
|
class WaveformWidget(QFrame):
|
||||||
"""Displays downsampled waveform with playhead position."""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setFixedHeight(60)
|
self.setFixedHeight(60)
|
||||||
@@ -25,7 +23,7 @@ class WaveformWidget(QFrame):
|
|||||||
QFrame {
|
QFrame {
|
||||||
background-color: #050505;
|
background-color: #050505;
|
||||||
border: 1px solid #1a1a1a;
|
border: 1px solid #1a1a1a;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -41,7 +39,6 @@ class WaveformWidget(QFrame):
|
|||||||
|
|
||||||
mono = np.mean(audio, axis=0)
|
mono = np.mean(audio, axis=0)
|
||||||
width = max(500, self.width())
|
width = max(500, self.width())
|
||||||
|
|
||||||
samples_per_pixel = max(1, len(mono) // width)
|
samples_per_pixel = max(1, len(mono) // width)
|
||||||
num_pixels = len(mono) // samples_per_pixel
|
num_pixels = len(mono) // samples_per_pixel
|
||||||
|
|
||||||
@@ -72,7 +69,6 @@ class WaveformWidget(QFrame):
|
|||||||
|
|
||||||
max_val = np.max(self.waveform_data) if np.max(self.waveform_data) > 0 else 1.0
|
max_val = np.max(self.waveform_data) if np.max(self.waveform_data) > 0 else 1.0
|
||||||
scaled = (self.waveform_data / max_val) * (height * 0.8)
|
scaled = (self.waveform_data / max_val) * (height * 0.8)
|
||||||
|
|
||||||
x_scale = width / len(self.waveform_data)
|
x_scale = width / len(self.waveform_data)
|
||||||
|
|
||||||
for i, rms in enumerate(self.waveform_data):
|
for i, rms in enumerate(self.waveform_data):
|
||||||
@@ -81,7 +77,6 @@ class WaveformWidget(QFrame):
|
|||||||
y_top = rect.y() + (height - bar_h) // 2
|
y_top = rect.y() + (height - bar_h) // 2
|
||||||
|
|
||||||
pct = i / len(self.waveform_data)
|
pct = i / len(self.waveform_data)
|
||||||
|
|
||||||
if pct < self.position_pct:
|
if pct < self.position_pct:
|
||||||
color = QColor("#004400")
|
color = QColor("#004400")
|
||||||
elif pct > 0.85:
|
elif pct > 0.85:
|
||||||
@@ -101,6 +96,80 @@ class WaveformWidget(QFrame):
|
|||||||
painter.drawLine(playhead_x, rect.y(), playhead_x, rect.y() + height)
|
painter.drawLine(playhead_x, rect.y(), playhead_x, rect.y() + height)
|
||||||
|
|
||||||
|
|
||||||
|
class WaveformMonitor(QFrame):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QFrame {
|
||||||
|
background-color: #050a05;
|
||||||
|
border: 1px solid #1a2a1a;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
rect = self.rect().adjusted(3, 3, -3, -3)
|
||||||
|
if rect.width() < 2 or rect.height() < 2:
|
||||||
|
painter.end()
|
||||||
|
return
|
||||||
|
|
||||||
|
painter.fillRect(rect, QColor("#050a05"))
|
||||||
|
|
||||||
|
pen = QPen(QColor("#1a2a1a"), 1)
|
||||||
|
painter.setPen(pen)
|
||||||
|
painter.drawRect(rect)
|
||||||
|
|
||||||
|
engine = getattr(self, '_engine', None)
|
||||||
|
monitor_key = getattr(self, '_monitor_key', None)
|
||||||
|
if engine is None or monitor_key is None:
|
||||||
|
painter.end()
|
||||||
|
return
|
||||||
|
|
||||||
|
monitor = engine.ext_monitors.get(monitor_key)
|
||||||
|
if monitor is None or not monitor.monitoring:
|
||||||
|
pen = QPen(QColor("#0a1a0a"), 1)
|
||||||
|
painter.setPen(pen)
|
||||||
|
mid_y = rect.center().y()
|
||||||
|
painter.drawLine(rect.left(), mid_y, rect.right(), mid_y)
|
||||||
|
painter.end()
|
||||||
|
return
|
||||||
|
|
||||||
|
buf = monitor.buffer
|
||||||
|
w = rect.width()
|
||||||
|
h = rect.height()
|
||||||
|
mid_y = rect.y() + h // 2
|
||||||
|
|
||||||
|
path = QPainterPath()
|
||||||
|
path.moveTo(rect.right(), mid_y)
|
||||||
|
for i in range(len(buf)):
|
||||||
|
x = rect.right() - int((i / len(buf)) * w)
|
||||||
|
val = buf[(monitor.write_pos - i) % len(buf)]
|
||||||
|
y = mid_y - int(val * (h * 0.45))
|
||||||
|
if i == 0:
|
||||||
|
path.lineTo(x, y)
|
||||||
|
else:
|
||||||
|
path.lineTo(x, y)
|
||||||
|
path.lineTo(rect.right(), mid_y)
|
||||||
|
path.closeSubpath()
|
||||||
|
|
||||||
|
painter.setPen(QPen(QColor("#00ff41"), 1))
|
||||||
|
painter.setBrush(QColor(0, 255, 65, 30))
|
||||||
|
painter.drawPath(path)
|
||||||
|
|
||||||
|
pen = QPen(QColor("#00ff41"), 1)
|
||||||
|
painter.setPen(pen)
|
||||||
|
for i in range(len(buf)):
|
||||||
|
x = rect.right() - int((i / len(buf)) * w)
|
||||||
|
val = buf[(monitor.write_pos - i) % len(buf)]
|
||||||
|
y = mid_y - int(val * (h * 0.45))
|
||||||
|
if val > 0.01:
|
||||||
|
painter.drawLine(x, y, x, mid_y)
|
||||||
|
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
|
||||||
class LCDDisplay(QFrame):
|
class LCDDisplay(QFrame):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -119,9 +188,7 @@ class LCDDisplay(QFrame):
|
|||||||
|
|
||||||
self.track_label = QLabel("NO TRACK LOADED")
|
self.track_label = QLabel("NO TRACK LOADED")
|
||||||
self.track_label.setFont(QFont("Courier New", 13, QFont.Bold))
|
self.track_label.setFont(QFont("Courier New", 13, QFont.Bold))
|
||||||
self.track_label.setStyleSheet(
|
self.track_label.setStyleSheet("color: #00ff41; background: transparent;")
|
||||||
"color: #00ff41; background: transparent;"
|
|
||||||
)
|
|
||||||
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
self.track_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
self.track_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
|
|
||||||
@@ -130,16 +197,12 @@ class LCDDisplay(QFrame):
|
|||||||
|
|
||||||
self.elapsed_label = QLabel("00:00")
|
self.elapsed_label = QLabel("00:00")
|
||||||
self.elapsed_label.setFont(QFont("Courier New", 32, QFont.Bold))
|
self.elapsed_label.setFont(QFont("Courier New", 32, QFont.Bold))
|
||||||
self.elapsed_label.setStyleSheet(
|
self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent;")
|
||||||
"color: #00ff41; background: transparent;"
|
|
||||||
)
|
|
||||||
self.elapsed_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
self.elapsed_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
|
||||||
self.remaining_label = QLabel("-00:00")
|
self.remaining_label = QLabel("-00:00")
|
||||||
self.remaining_label.setFont(QFont("Courier New", 18, QFont.Bold))
|
self.remaining_label.setFont(QFont("Courier New", 18, QFont.Bold))
|
||||||
self.remaining_label.setStyleSheet(
|
self.remaining_label.setStyleSheet("color: #00cc33; background: transparent;")
|
||||||
"color: #00cc33; background: transparent;"
|
|
||||||
)
|
|
||||||
self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
|
|
||||||
time_row.addWidget(self.elapsed_label)
|
time_row.addWidget(self.elapsed_label)
|
||||||
@@ -150,15 +213,11 @@ class LCDDisplay(QFrame):
|
|||||||
|
|
||||||
self.status_label = QLabel("STOPPED")
|
self.status_label = QLabel("STOPPED")
|
||||||
self.status_label.setFont(QFont("Courier New", 11, QFont.Bold))
|
self.status_label.setFont(QFont("Courier New", 11, QFont.Bold))
|
||||||
self.status_label.setStyleSheet(
|
self.status_label.setStyleSheet("color: #008822; background: transparent;")
|
||||||
"color: #008822; background: transparent;"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.queue_label = QLabel("QUEUE: EMPTY")
|
self.queue_label = QLabel("QUEUE: EMPTY")
|
||||||
self.queue_label.setFont(QFont("Courier New", 10))
|
self.queue_label.setFont(QFont("Courier New", 10))
|
||||||
self.queue_label.setStyleSheet(
|
self.queue_label.setStyleSheet("color: #008822; background: transparent;")
|
||||||
"color: #008822; background: transparent;"
|
|
||||||
)
|
|
||||||
self.queue_label.setAlignment(Qt.AlignRight)
|
self.queue_label.setAlignment(Qt.AlignRight)
|
||||||
|
|
||||||
status_row.addWidget(self.status_label)
|
status_row.addWidget(self.status_label)
|
||||||
@@ -190,33 +249,19 @@ class LCDDisplay(QFrame):
|
|||||||
|
|
||||||
def set_playing(self, playing: bool):
|
def set_playing(self, playing: bool):
|
||||||
colour = "#00ff41" if playing else "#00aa2a"
|
colour = "#00ff41" if playing else "#00aa2a"
|
||||||
self.elapsed_label.setStyleSheet(
|
self.elapsed_label.setStyleSheet(f"color: {colour}; background: transparent;")
|
||||||
f"color: {colour}; background: transparent;"
|
|
||||||
)
|
|
||||||
|
|
||||||
def flash_warning(self, active: bool):
|
def flash_warning(self, active: bool):
|
||||||
"""Flash all time displays red when in warning zone."""
|
|
||||||
if active:
|
if active:
|
||||||
self.elapsed_label.setStyleSheet(
|
self.elapsed_label.setStyleSheet("color: #ff4444; background: transparent;")
|
||||||
"color: #ff4444; background: transparent;"
|
self.remaining_label.setStyleSheet("color: #ff4444; background: transparent;")
|
||||||
)
|
|
||||||
self.remaining_label.setStyleSheet(
|
|
||||||
"color: #ff4444; background: transparent;"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.elapsed_label.setStyleSheet(
|
self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent;")
|
||||||
"color: #00ff41; background: transparent;"
|
self.remaining_label.setStyleSheet("color: #00cc33; background: transparent;")
|
||||||
)
|
|
||||||
self.remaining_label.setStyleSheet(
|
|
||||||
"color: #00cc33; background: transparent;"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def make_icon_button(icon: QIcon,
|
def make_icon_button(icon, color="#3a3a3a", icon_color="white",
|
||||||
color: str = "#3a3a3a",
|
width=52, height=48):
|
||||||
icon_color: str = "white",
|
|
||||||
width: int = 52,
|
|
||||||
height: int = 48) -> QPushButton:
|
|
||||||
btn = QPushButton()
|
btn = QPushButton()
|
||||||
btn.setFixedSize(width, height)
|
btn.setFixedSize(width, height)
|
||||||
btn.setIconSize(QSize(22, 22))
|
btn.setIconSize(QSize(22, 22))
|
||||||
@@ -250,9 +295,7 @@ class DeckWidget(QWidget):
|
|||||||
self.current_theme = None
|
self.current_theme = None
|
||||||
self.flash_state = False
|
self.flash_state = False
|
||||||
self._playing_icon = False
|
self._playing_icon = False
|
||||||
self._playing_icon = False
|
|
||||||
|
|
||||||
# Convert "DECK 1" to "DECK_001"
|
|
||||||
deck_num = label.split()[-1]
|
deck_num = label.split()[-1]
|
||||||
self.deck_label = f"DECK_{int(deck_num):03d}"
|
self.deck_label = f"DECK_{int(deck_num):03d}"
|
||||||
|
|
||||||
@@ -321,10 +364,9 @@ class DeckWidget(QWidget):
|
|||||||
|
|
||||||
def apply_theme(self, theme: dict):
|
def apply_theme(self, theme: dict):
|
||||||
self.current_theme = theme
|
self.current_theme = theme
|
||||||
|
|
||||||
border_color = theme['border']
|
border_color = theme['border']
|
||||||
bg_color = theme['bg2']
|
bg_color = theme['bg2']
|
||||||
|
|
||||||
self.setStyleSheet(f"""
|
self.setStyleSheet(f"""
|
||||||
QWidget {{
|
QWidget {{
|
||||||
background-color: {bg_color};
|
background-color: {bg_color};
|
||||||
@@ -332,7 +374,6 @@ class DeckWidget(QWidget):
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self.lcd.setStyleSheet(f"""
|
self.lcd.setStyleSheet(f"""
|
||||||
QFrame {{
|
QFrame {{
|
||||||
background-color: {theme['bg']};
|
background-color: {theme['bg']};
|
||||||
@@ -340,7 +381,6 @@ class DeckWidget(QWidget):
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self.waveform.setStyleSheet(f"""
|
self.waveform.setStyleSheet(f"""
|
||||||
QFrame {{
|
QFrame {{
|
||||||
background-color: {theme['bg']};
|
background-color: {theme['bg']};
|
||||||
@@ -466,7 +506,6 @@ class DeckWidget(QWidget):
|
|||||||
def _refresh_display(self):
|
def _refresh_display(self):
|
||||||
deck = self.engine.decks[self.deck_key]
|
deck = self.engine.decks[self.deck_key]
|
||||||
|
|
||||||
# Auto-load queued track when current finishes
|
|
||||||
if not deck.playing and deck.track is not None:
|
if not deck.playing and deck.track is not None:
|
||||||
if deck.position >= deck.track.num_samples - 1:
|
if deck.position >= deck.track.num_samples - 1:
|
||||||
if deck.queued:
|
if deck.queued:
|
||||||
@@ -477,7 +516,6 @@ class DeckWidget(QWidget):
|
|||||||
self.lcd.update_queue("")
|
self.lcd.update_queue("")
|
||||||
self.lcd.update_status("LOADED")
|
self.lcd.update_status("LOADED")
|
||||||
|
|
||||||
# Sync button if stopped manually
|
|
||||||
if not deck.playing and self._playing_icon:
|
if not deck.playing and self._playing_icon:
|
||||||
self.btn_play.setIcon(icon_play("#00ff41"))
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
||||||
self._playing_icon = False
|
self._playing_icon = False
|
||||||
@@ -493,33 +531,21 @@ class DeckWidget(QWidget):
|
|||||||
self.lcd.set_playing(False)
|
self.lcd.set_playing(False)
|
||||||
|
|
||||||
if deck.track:
|
if deck.track:
|
||||||
self.lcd.update_time(
|
self.lcd.update_time(deck.get_elapsed(), deck.get_remaining())
|
||||||
deck.get_elapsed(),
|
|
||||||
deck.get_remaining()
|
|
||||||
)
|
|
||||||
|
|
||||||
position_pct = deck.position / deck.track.num_samples
|
position_pct = deck.position / deck.track.num_samples
|
||||||
remaining_sec = deck.track.duration - (deck.position / deck.track.sr)
|
remaining_sec = deck.track.duration - (deck.position / deck.track.sr)
|
||||||
|
|
||||||
# Warning if < 20 seconds OR < 15% remaining
|
|
||||||
in_warning = (remaining_sec < 20) or (position_pct > 0.85)
|
in_warning = (remaining_sec < 20) or (position_pct > 0.85)
|
||||||
|
|
||||||
# Flash header AND all numbers if in warning zone and playing
|
|
||||||
if deck.playing and in_warning:
|
if deck.playing and in_warning:
|
||||||
flash_color = '#ff4444' if self.flash_state else self.accent_color
|
flash_color = '#ff4444' if self.flash_state else self.accent_color
|
||||||
|
|
||||||
# Flash header
|
|
||||||
self.header.setStyleSheet(f"""
|
self.header.setStyleSheet(f"""
|
||||||
color: {flash_color};
|
color: {flash_color};
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Flash LCD numbers
|
|
||||||
self.lcd.flash_warning(self.flash_state)
|
self.lcd.flash_warning(self.flash_state)
|
||||||
else:
|
else:
|
||||||
# Normal colors
|
|
||||||
self.header.setStyleSheet(f"""
|
self.header.setStyleSheet(f"""
|
||||||
color: {self.accent_color};
|
color: {self.accent_color};
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -527,10 +553,121 @@ class DeckWidget(QWidget):
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
""")
|
""")
|
||||||
self.lcd.flash_warning(False)
|
self.lcd.flash_warning(False)
|
||||||
|
|
||||||
self.waveform.set_position(position_pct, in_warning and self.flash_state)
|
self.waveform.set_position(position_pct, in_warning and self.flash_state)
|
||||||
|
|
||||||
if deck.queued:
|
if deck.queued:
|
||||||
self.lcd.update_queue(
|
self.lcd.update_queue(os.path.basename(deck.queued.filepath))
|
||||||
os.path.basename(deck.queued.filepath)
|
|
||||||
)
|
|
||||||
|
class ExternalDeckWidget(QWidget):
|
||||||
|
def __init__(self, deck_key: str, label: str, accent_color: str,
|
||||||
|
engine, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.deck_key = deck_key
|
||||||
|
self.engine = engine
|
||||||
|
self.accent_color = accent_color
|
||||||
|
self._monitoring = False
|
||||||
|
|
||||||
|
deck_num = label.split()[-1]
|
||||||
|
self.deck_label = f"EXT_{int(deck_num):03d}"
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
self.setStyleSheet(f"""
|
||||||
|
QWidget {{
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
border: 2px solid {self.accent_color};
|
||||||
|
border-radius: 6px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
root = QVBoxLayout(self)
|
||||||
|
root.setContentsMargins(10, 10, 10, 10)
|
||||||
|
root.setSpacing(8)
|
||||||
|
|
||||||
|
self.header = QLabel(self.deck_label)
|
||||||
|
self.header.setFont(QFont("Courier New", 24, QFont.Bold))
|
||||||
|
self.header.setStyleSheet(f"""
|
||||||
|
color: {self.accent_color};
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
""")
|
||||||
|
self.header.setAlignment(Qt.AlignCenter)
|
||||||
|
root.addWidget(self.header)
|
||||||
|
|
||||||
|
self.waveform = WaveformMonitor()
|
||||||
|
self.waveform._engine = self.engine
|
||||||
|
self.waveform._monitor_key = self.deck_key
|
||||||
|
self.waveform.setMinimumHeight(100)
|
||||||
|
root.addWidget(self.waveform, stretch=1)
|
||||||
|
|
||||||
|
transport = QHBoxLayout()
|
||||||
|
transport.setSpacing(6)
|
||||||
|
|
||||||
|
self.btn_play = make_icon_button(icon_play(), "#1a6b3a", "#00ff41", 68, 48)
|
||||||
|
transport.addStretch()
|
||||||
|
transport.addWidget(self.btn_play)
|
||||||
|
transport.addStretch()
|
||||||
|
|
||||||
|
root.addLayout(transport)
|
||||||
|
|
||||||
|
self._connect_signals()
|
||||||
|
|
||||||
|
def _connect_signals(self):
|
||||||
|
self.btn_play.clicked.connect(self._toggle_monitor)
|
||||||
|
|
||||||
|
def toggle_play(self):
|
||||||
|
self._toggle_monitor()
|
||||||
|
|
||||||
|
def _toggle_monitor(self):
|
||||||
|
monitor = self.engine.ext_monitors[self.deck_key]
|
||||||
|
self._monitoring = not self._monitoring
|
||||||
|
monitor.monitoring = self._monitoring
|
||||||
|
|
||||||
|
if self._monitoring:
|
||||||
|
self.btn_play.setIcon(icon_pause("#ffffff"))
|
||||||
|
self.btn_play.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #00aa33;
|
||||||
|
border: 1px solid #00ff41;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #00cc44; }
|
||||||
|
""")
|
||||||
|
self.header.setStyleSheet(f"""
|
||||||
|
color: #00ff41;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
""")
|
||||||
|
else:
|
||||||
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
||||||
|
self.btn_play.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #1a6b3a;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #2a7b4a; }
|
||||||
|
""")
|
||||||
|
self.header.setStyleSheet(f"""
|
||||||
|
color: {self.accent_color};
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
""")
|
||||||
|
self.waveform.update()
|
||||||
|
|
||||||
|
def apply_theme(self, theme: dict):
|
||||||
|
border_color = theme['border']
|
||||||
|
bg_color = theme['bg2']
|
||||||
|
self.setStyleSheet(f"""
|
||||||
|
QWidget {{
|
||||||
|
background-color: {bg_color};
|
||||||
|
border: 2px solid {border_color};
|
||||||
|
border-radius: 6px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|||||||
@@ -279,6 +279,72 @@ def icon_solo(color: str = "#ffaa00") -> QIcon:
|
|||||||
return _make_icon(paint, color)
|
return _make_icon(paint, color)
|
||||||
|
|
||||||
|
|
||||||
|
def icon_cog(color: str = "#aaaaaa") -> QIcon:
|
||||||
|
def paint(p, r, c):
|
||||||
|
pen = QPen(c, r.width() * 0.1)
|
||||||
|
pen.setCapStyle(Qt.RoundCap)
|
||||||
|
p.setPen(pen)
|
||||||
|
p.setBrush(Qt.NoBrush)
|
||||||
|
# Outer circle (gear ring)
|
||||||
|
cx, cy = r.center().x(), r.center().y()
|
||||||
|
outer_r = min(r.width(), r.height()) * 0.42
|
||||||
|
inner_r = outer_r * 0.6
|
||||||
|
# Draw gear teeth (6 bumps)
|
||||||
|
import math
|
||||||
|
p.setBrush(QColor(c))
|
||||||
|
for i in range(6):
|
||||||
|
angle = i * 60 - 30
|
||||||
|
a_rad = math.radians(angle)
|
||||||
|
bx = cx + outer_r * math.cos(a_rad)
|
||||||
|
by = cy + outer_r * math.sin(a_rad)
|
||||||
|
tooth = QPainterPath()
|
||||||
|
tw = r.width() * 0.12
|
||||||
|
th = r.width() * 0.18
|
||||||
|
tooth.addRect(bx - tw/2, by - th/2, tw, th)
|
||||||
|
p.drawRect(bx - tw/2, by - th/2, tw, th)
|
||||||
|
# Center circle
|
||||||
|
p.setPen(Qt.NoPen)
|
||||||
|
p.setBrush(QColor(c))
|
||||||
|
p.drawEllipse(QPointF(cx, cy), inner_r, inner_r)
|
||||||
|
# Inner hole
|
||||||
|
p.setBrush(QColor("#000000"))
|
||||||
|
p.drawEllipse(QPointF(cx, cy), inner_r * 0.55, inner_r * 0.55)
|
||||||
|
return _make_icon(paint, color)
|
||||||
|
|
||||||
|
|
||||||
|
def icon_cog(color: str = "#aaaaaa") -> QIcon:
|
||||||
|
def paint(p, r, c):
|
||||||
|
pen = QPen(c, r.width() * 0.1)
|
||||||
|
pen.setCapStyle(Qt.RoundCap)
|
||||||
|
p.setPen(pen)
|
||||||
|
p.setBrush(Qt.NoBrush)
|
||||||
|
# Outer circle (gear ring)
|
||||||
|
cx, cy = r.center().x(), r.center().y()
|
||||||
|
outer_r = min(r.width(), r.height()) * 0.42
|
||||||
|
inner_r = outer_r * 0.6
|
||||||
|
# Draw gear teeth (6 bumps)
|
||||||
|
import math
|
||||||
|
p.setBrush(QColor(c))
|
||||||
|
for i in range(6):
|
||||||
|
angle = i * 60 - 30
|
||||||
|
a_rad = math.radians(angle)
|
||||||
|
bx = cx + outer_r * math.cos(a_rad)
|
||||||
|
by = cy + outer_r * math.sin(a_rad)
|
||||||
|
tooth = QPainterPath()
|
||||||
|
tw = r.width() * 0.12
|
||||||
|
th = r.width() * 0.18
|
||||||
|
tooth.addRect(bx - tw/2, by - th/2, tw, th)
|
||||||
|
p.drawRect(bx - tw/2, by - th/2, tw, th)
|
||||||
|
# Center circle
|
||||||
|
p.setPen(Qt.NoPen)
|
||||||
|
p.setBrush(QColor(c))
|
||||||
|
p.drawEllipse(QPointF(cx, cy), inner_r, inner_r)
|
||||||
|
# Inner hole
|
||||||
|
p.setBrush(QColor("#000000"))
|
||||||
|
p.drawEllipse(QPointF(cx, cy), inner_r * 0.55, inner_r * 0.55)
|
||||||
|
return _make_icon(paint, color)
|
||||||
|
|
||||||
|
|
||||||
def icon_theme(color: str = "#00ff41") -> QIcon:
|
def icon_theme(color: str = "#00ff41") -> QIcon:
|
||||||
def paint(p, r, c):
|
def paint(p, r, c):
|
||||||
p.setPen(Qt.NoPen)
|
p.setPen(Qt.NoPen)
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
# main.py
|
# main.py
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget,
|
QApplication, QMainWindow, QWidget, QFileDialog,
|
||||||
QVBoxLayout, QHBoxLayout, QSplitter
|
QVBoxLayout, QHBoxLayout, QSplitter
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtGui import QFont, QColor, QPalette
|
from PySide6.QtGui import QFont, QColor, QPalette
|
||||||
|
|
||||||
from audio_engine import AudioEngine
|
from audio_engine import AudioEngine
|
||||||
from deck_widget import DeckWidget
|
from deck_widget import DeckWidget, ExternalDeckWidget
|
||||||
from cart_widget import CartBankWidget
|
from cart_widget import CartBankWidget
|
||||||
from playlist_widget import PlaylistWidget
|
from playlist_widget import PlaylistWidget
|
||||||
from midi_engine import MidiEngine
|
from midi_engine import MidiEngine
|
||||||
@@ -25,7 +27,7 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
self.engine = AudioEngine()
|
self.engine = AudioEngine()
|
||||||
|
|
||||||
self.setWindowTitle("Getting To It")
|
self.setWindowTitle("Getting To It")
|
||||||
self.setMinimumSize(1280, 720)
|
self.setMinimumSize(1400, 860)
|
||||||
self.resize(1920, 1080)
|
self.resize(1920, 1080)
|
||||||
self._apply_global_style(THEMES[0])
|
self._apply_global_style(THEMES[0])
|
||||||
|
|
||||||
@@ -33,31 +35,26 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
self.setCentralWidget(central)
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
outer = QVBoxLayout(central)
|
outer = QVBoxLayout(central)
|
||||||
outer.setContentsMargins(8, 8, 8, 8)
|
outer.setContentsMargins(6, 6, 6, 6)
|
||||||
outer.setSpacing(8)
|
outer.setSpacing(6)
|
||||||
|
|
||||||
# Top bar
|
# Top bar
|
||||||
self.top_bar = TopBarWidget()
|
self.top_bar = TopBarWidget()
|
||||||
self.top_bar.theme_changed.connect(self._on_theme_changed)
|
self.top_bar.theme_changed.connect(self._on_theme_changed)
|
||||||
|
self.top_bar.ratio_changed.connect(self._apply_ratio)
|
||||||
|
self.top_bar.save_session.connect(self._save_session)
|
||||||
|
self.top_bar.load_session.connect(self._load_session)
|
||||||
outer.addWidget(self.top_bar)
|
outer.addWidget(self.top_bar)
|
||||||
|
|
||||||
# ── Build widgets ─────────────────────────────────────────────────
|
# ── Build widgets ─────────────────────────────────────────────────
|
||||||
|
|
||||||
self.deck1 = DeckWidget(
|
self.deck1 = DeckWidget("deck1", "DECK 1", "#00b894", self.engine)
|
||||||
deck_key="deck1",
|
self.deck2 = DeckWidget("deck2", "DECK 2", "#6c5ce7", self.engine)
|
||||||
label="DECK 1",
|
self.deck3 = ExternalDeckWidget("deck3", "DECK 3", "#fdcb6e", self.engine)
|
||||||
accent_color="#00897b",
|
self.deck4 = ExternalDeckWidget("deck4", "DECK 4", "#e17055", self.engine)
|
||||||
engine=self.engine
|
|
||||||
)
|
|
||||||
self.deck2 = DeckWidget(
|
|
||||||
deck_key="deck2",
|
|
||||||
label="DECK 2",
|
|
||||||
accent_color="#f9ca24",
|
|
||||||
engine=self.engine
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cart_bank = CartBankWidget(self.engine)
|
self.cart_bank = CartBankWidget(self.engine)
|
||||||
self.cart_bank.setFixedHeight(280)
|
self.cart_bank.setFixedHeight(220)
|
||||||
|
|
||||||
self.playlist_widget = PlaylistWidget(
|
self.playlist_widget = PlaylistWidget(
|
||||||
engine=self.engine,
|
engine=self.engine,
|
||||||
@@ -72,46 +69,52 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
|
|
||||||
self.mixer = MixerWidget(self.engine)
|
self.mixer = MixerWidget(self.engine)
|
||||||
|
|
||||||
# ── Centre column (decks + carts + mixer) ─────────────────────────
|
# ── Centre column with vertical splitter ─────────────────────────
|
||||||
centre_col = QVBoxLayout()
|
centre_col = QVBoxLayout()
|
||||||
centre_col.setSpacing(8)
|
centre_col.setSpacing(6)
|
||||||
|
|
||||||
# Top: decks row + carts
|
# Top: 4 decks in 2x2 grid + carts
|
||||||
deck_area = QWidget()
|
deck_area = QWidget()
|
||||||
deck_area.setStyleSheet("background: transparent; border: none;")
|
deck_area.setStyleSheet("background: transparent; border: none;")
|
||||||
deck_layout = QVBoxLayout(deck_area)
|
deck_layout = QVBoxLayout(deck_area)
|
||||||
deck_layout.setContentsMargins(0, 0, 0, 0)
|
deck_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
deck_layout.setSpacing(8)
|
deck_layout.setSpacing(4)
|
||||||
|
|
||||||
|
deck_row1 = QHBoxLayout()
|
||||||
|
deck_row1.setSpacing(4)
|
||||||
|
deck_row1.addWidget(self.deck1)
|
||||||
|
deck_row1.addWidget(self.deck2)
|
||||||
|
deck_layout.addLayout(deck_row1)
|
||||||
|
|
||||||
|
deck_row2 = QHBoxLayout()
|
||||||
|
deck_row2.setSpacing(4)
|
||||||
|
deck_row2.addWidget(self.deck3)
|
||||||
|
deck_row2.addWidget(self.deck4)
|
||||||
|
deck_layout.addLayout(deck_row2)
|
||||||
|
|
||||||
deck_row = QHBoxLayout()
|
|
||||||
deck_row.setSpacing(8)
|
|
||||||
deck_row.addWidget(self.deck1)
|
|
||||||
deck_row.addWidget(self.deck2)
|
|
||||||
deck_layout.addLayout(deck_row)
|
|
||||||
deck_layout.addWidget(self.cart_bank)
|
deck_layout.addWidget(self.cart_bank)
|
||||||
deck_layout.addStretch()
|
|
||||||
|
|
||||||
# Bottom: mixer
|
# Mixer fills remaining space below decks
|
||||||
centre_col.addWidget(deck_area, stretch=1)
|
centre_col.addWidget(deck_area)
|
||||||
centre_col.addWidget(self.mixer)
|
centre_col.addWidget(self.mixer, stretch=1)
|
||||||
|
|
||||||
centre_widget = QWidget()
|
centre_widget = QWidget()
|
||||||
centre_widget.setStyleSheet("background: transparent; border: none;")
|
centre_widget.setStyleSheet("background: transparent; border: none;")
|
||||||
centre_widget.setLayout(centre_col)
|
centre_widget.setLayout(centre_col)
|
||||||
|
|
||||||
# ── Splitter ──────────────────────────────────────────────────────
|
# ── Horizontal splitter ──────────────────────────────────────────
|
||||||
self.splitter = QSplitter(Qt.Horizontal)
|
self.splitter = QSplitter(Qt.Horizontal)
|
||||||
self.splitter.setHandleWidth(6)
|
self.splitter.setHandleWidth(4)
|
||||||
self._style_splitter(THEMES[0])
|
self._style_splitter(THEMES[0])
|
||||||
|
|
||||||
self.splitter.addWidget(self.playlist_widget)
|
self.splitter.addWidget(self.playlist_widget)
|
||||||
self.splitter.addWidget(centre_widget)
|
self.splitter.addWidget(centre_widget)
|
||||||
self.splitter.addWidget(self.history_widget)
|
self.splitter.addWidget(self.history_widget)
|
||||||
self.splitter.setSizes([400, 1120, 400])
|
self.splitter.setSizes([360, 1160, 360])
|
||||||
|
|
||||||
outer.addWidget(self.splitter)
|
outer.addWidget(self.splitter)
|
||||||
|
|
||||||
# ── Wire signals ──────────────────────────────────────────────────
|
# ── Wire signals ─────────────────────────────────────────────────
|
||||||
self.deck1.track_started.connect(self.history_widget.add_track)
|
self.deck1.track_started.connect(self.history_widget.add_track)
|
||||||
self.deck2.track_started.connect(self.history_widget.add_track)
|
self.deck2.track_started.connect(self.history_widget.add_track)
|
||||||
|
|
||||||
@@ -121,8 +124,8 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
def _style_splitter(self, theme: dict):
|
def _style_splitter(self, theme: dict):
|
||||||
self.splitter.setStyleSheet(f"""
|
self.splitter.setStyleSheet(f"""
|
||||||
QSplitter::handle {{
|
QSplitter::handle {{
|
||||||
background-color: #333333;
|
background-color: #22242a;
|
||||||
border-radius: 3px;
|
border-radius: 2px;
|
||||||
}}
|
}}
|
||||||
QSplitter::handle:hover {{
|
QSplitter::handle:hover {{
|
||||||
background-color: {theme['accent']};
|
background-color: {theme['accent']};
|
||||||
@@ -137,10 +140,15 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
self._style_splitter(theme)
|
self._style_splitter(theme)
|
||||||
self.deck1.apply_theme(theme)
|
self.deck1.apply_theme(theme)
|
||||||
self.deck2.apply_theme(theme)
|
self.deck2.apply_theme(theme)
|
||||||
|
self.deck3.apply_theme(theme)
|
||||||
|
self.deck4.apply_theme(theme)
|
||||||
self.cart_bank.apply_theme(theme)
|
self.cart_bank.apply_theme(theme)
|
||||||
self.history_widget.apply_theme(theme)
|
self.history_widget.apply_theme(theme)
|
||||||
self.mixer.apply_theme(theme)
|
self.mixer.apply_theme(theme)
|
||||||
|
|
||||||
|
def _apply_ratio(self, w, h):
|
||||||
|
self.resize(w, h)
|
||||||
|
|
||||||
def _apply_global_style(self, theme: dict):
|
def _apply_global_style(self, theme: dict):
|
||||||
self.setStyleSheet(f"""
|
self.setStyleSheet(f"""
|
||||||
QMainWindow {{
|
QMainWindow {{
|
||||||
@@ -149,7 +157,7 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
QWidget {{
|
QWidget {{
|
||||||
background-color: {theme['bg']};
|
background-color: {theme['bg']};
|
||||||
color: {theme['text']};
|
color: {theme['text']};
|
||||||
font-family: Arial;
|
font-family: "Segoe UI", Arial, sans-serif;
|
||||||
}}
|
}}
|
||||||
QToolTip {{
|
QToolTip {{
|
||||||
background-color: {theme['bg2']};
|
background-color: {theme['bg2']};
|
||||||
@@ -159,6 +167,60 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
def _save_session(self):
|
||||||
|
path, _ = QFileDialog.getSaveFileName(
|
||||||
|
self.top_bar, "Save Session",
|
||||||
|
os.path.expanduser("~/.gti_radiostudio/session.json"),
|
||||||
|
"JSON Files (*.json)"
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
state = {
|
||||||
|
'window_size': (self.width(), self.height()),
|
||||||
|
'channels': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for ch in self.engine.mixer['channels']:
|
||||||
|
state['channels'].append({
|
||||||
|
'name': ch.name,
|
||||||
|
'volume': ch.volume,
|
||||||
|
'muted': ch.muted,
|
||||||
|
'solo': ch.solo,
|
||||||
|
'pfl': ch.pfl,
|
||||||
|
})
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
def _load_session(self):
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self.top_bar, "Load Session",
|
||||||
|
os.path.expanduser("~/.gti_radiostudio/session.json"),
|
||||||
|
"JSON Files (*.json)"
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(path) as f:
|
||||||
|
state = json.load(f)
|
||||||
|
|
||||||
|
ws = state.get('window_size')
|
||||||
|
if ws:
|
||||||
|
self.resize(ws[0], ws[1])
|
||||||
|
|
||||||
|
for i, ch_state in enumerate(state.get('channels', [])):
|
||||||
|
if i < len(self.engine.mixer['channels']):
|
||||||
|
ch = self.engine.mixer['channels'][i]
|
||||||
|
ch.name = ch_state.get('name', ch.name)
|
||||||
|
ch.volume = ch_state.get('volume', ch.volume)
|
||||||
|
ch.muted = ch_state.get('muted', ch.muted)
|
||||||
|
ch.solo = ch_state.get('solo', ch.solo)
|
||||||
|
ch.pfl = ch_state.get('pfl', ch.pfl)
|
||||||
|
|
||||||
|
self.mixer.refresh_strips()
|
||||||
|
|
||||||
def _setup_midi(self):
|
def _setup_midi(self):
|
||||||
self.midi = MidiEngine()
|
self.midi = MidiEngine()
|
||||||
|
|
||||||
@@ -168,15 +230,27 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
self.midi.cart_trigger.connect(self._on_midi_cart)
|
self.midi.cart_trigger.connect(self._on_midi_cart)
|
||||||
self.midi.mixer_volume.connect(self._on_midi_mixer_volume)
|
self.midi.mixer_volume.connect(self._on_midi_mixer_volume)
|
||||||
self.midi.mixer_mute.connect(self._on_midi_mixer_mute)
|
self.midi.mixer_mute.connect(self._on_midi_mixer_mute)
|
||||||
|
self.midi.mixer_solo.connect(self._on_midi_mixer_solo)
|
||||||
|
self.midi.mixer_pfl.connect(self._on_midi_mixer_pfl)
|
||||||
|
self.midi.master_control.connect(self._on_midi_master_control)
|
||||||
self.midi.controller_connected.connect(self._on_midi_status)
|
self.midi.controller_connected.connect(self._on_midi_status)
|
||||||
|
|
||||||
self.midi.start()
|
self.midi.start()
|
||||||
|
self.top_bar.set_midi_engine(self.midi)
|
||||||
|
|
||||||
|
ports = self.midi.list_ports()
|
||||||
|
if ports:
|
||||||
|
print(f"[MIDI] Available ports: {ports}")
|
||||||
|
|
||||||
def _on_midi_play_pause(self, deck: str):
|
def _on_midi_play_pause(self, deck: str):
|
||||||
if deck == 'deck1':
|
if deck == 'deck1':
|
||||||
self.deck1.toggle_play()
|
self.deck1.toggle_play()
|
||||||
elif deck == 'deck2':
|
elif deck == 'deck2':
|
||||||
self.deck2.toggle_play()
|
self.deck2.toggle_play()
|
||||||
|
elif deck == 'deck3':
|
||||||
|
self.deck3.toggle_play()
|
||||||
|
elif deck == 'deck4':
|
||||||
|
self.deck4.toggle_play()
|
||||||
|
|
||||||
def _on_midi_cue(self, deck: str):
|
def _on_midi_cue(self, deck: str):
|
||||||
if deck == 'deck1':
|
if deck == 'deck1':
|
||||||
@@ -199,6 +273,31 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
def _on_midi_mixer_mute(self, channel: int, muted: bool):
|
def _on_midi_mixer_mute(self, channel: int, muted: bool):
|
||||||
self.engine.set_channel_mute(channel, muted)
|
self.engine.set_channel_mute(channel, muted)
|
||||||
|
|
||||||
|
def _on_midi_mixer_solo(self, channel: int, on: bool):
|
||||||
|
self.engine.set_channel_solo(channel, on)
|
||||||
|
|
||||||
|
def _on_midi_mixer_pfl(self, channel: int, on: bool):
|
||||||
|
self.engine.set_channel_pfl(channel, on)
|
||||||
|
|
||||||
|
def _on_midi_master_control(self, knob: str, val: float):
|
||||||
|
master = self.engine.mixer['master']
|
||||||
|
attrs = {
|
||||||
|
"threshold_db": (-60, 0),
|
||||||
|
"ratio": (1, 20),
|
||||||
|
"attack_ms": (0.1, 50),
|
||||||
|
"release_ms": (10, 1000),
|
||||||
|
"makeup_gain_db": (0, 24),
|
||||||
|
"ceiling_db": (-10, 0),
|
||||||
|
}
|
||||||
|
if knob in attrs:
|
||||||
|
lo, hi = attrs[knob]
|
||||||
|
v = lo + val * (hi - lo)
|
||||||
|
if knob == "ratio":
|
||||||
|
v = round(max(1, v), 1)
|
||||||
|
elif knob in ("threshold_db", "ceiling_db", "makeup_gain_db"):
|
||||||
|
v = round(max(lo, min(hi, v)), 1)
|
||||||
|
setattr(master.compressor if knob != "ceiling_db" else master.limiter, knob, v)
|
||||||
|
|
||||||
def _on_midi_status(self, connected: bool, name: str):
|
def _on_midi_status(self, connected: bool, name: str):
|
||||||
if connected:
|
if connected:
|
||||||
print(f"[MIDI] Connected: {name}")
|
print(f"[MIDI] Connected: {name}")
|
||||||
@@ -217,14 +316,14 @@ def main():
|
|||||||
app.setOrganizationName("Getting To It")
|
app.setOrganizationName("Getting To It")
|
||||||
|
|
||||||
palette = QPalette()
|
palette = QPalette()
|
||||||
palette.setColor(QPalette.Window, QColor("#0d0d0d"))
|
palette.setColor(QPalette.Window, QColor("#121418"))
|
||||||
palette.setColor(QPalette.WindowText, QColor("#cccccc"))
|
palette.setColor(QPalette.WindowText, QColor("#d0d4dc"))
|
||||||
palette.setColor(QPalette.Base, QColor("#111111"))
|
palette.setColor(QPalette.Base, QColor("#1a1c23"))
|
||||||
palette.setColor(QPalette.AlternateBase, QColor("#1a1a1a"))
|
palette.setColor(QPalette.AlternateBase, QColor("#22242c"))
|
||||||
palette.setColor(QPalette.Text, QColor("#cccccc"))
|
palette.setColor(QPalette.Text, QColor("#d0d4dc"))
|
||||||
palette.setColor(QPalette.Button, QColor("#2a2a2a"))
|
palette.setColor(QPalette.Button, QColor("#2a2c35"))
|
||||||
palette.setColor(QPalette.ButtonText, QColor("#cccccc"))
|
palette.setColor(QPalette.ButtonText, QColor("#d0d4dc"))
|
||||||
palette.setColor(QPalette.Highlight, QColor("#1a3a5a"))
|
palette.setColor(QPalette.Highlight, QColor("#00b894"))
|
||||||
palette.setColor(QPalette.HighlightedText, QColor("#ffffff"))
|
palette.setColor(QPalette.HighlightedText, QColor("#ffffff"))
|
||||||
app.setPalette(palette)
|
app.setPalette(palette)
|
||||||
|
|
||||||
@@ -235,4 +334,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
+34
-1
@@ -88,6 +88,9 @@ class MidiEngine(QObject):
|
|||||||
cart_trigger = Signal(int)
|
cart_trigger = Signal(int)
|
||||||
mixer_volume = Signal(int, float)
|
mixer_volume = Signal(int, float)
|
||||||
mixer_mute = Signal(int, bool)
|
mixer_mute = Signal(int, bool)
|
||||||
|
mixer_solo = Signal(int, bool)
|
||||||
|
mixer_pfl = Signal(int, bool)
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -226,12 +229,42 @@ class MidiEngine(QObject):
|
|||||||
target_id = binding['target_id']
|
target_id = binding['target_id']
|
||||||
inv = binding.get('inverse', False)
|
inv = binding.get('inverse', False)
|
||||||
|
|
||||||
if action == 'mixer_volume':
|
if 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 == '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':
|
||||||
scaled = (127 - value if inv else value) / 127.0
|
scaled = (127 - value if inv else value) / 127.0
|
||||||
self.mixer_volume.emit(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)
|
||||||
|
elif action == 'mixer_solo':
|
||||||
|
on = (value > 63) if not inv else (value < 64)
|
||||||
|
self.mixer_solo.emit(target_id, on)
|
||||||
|
elif action == 'mixer_pfl':
|
||||||
|
on = (value > 63) if not inv else (value < 64)
|
||||||
|
self.mixer_pfl.emit(target_id, on)
|
||||||
|
elif action == 'master_control':
|
||||||
|
scaled = (127 - value if inv else value) / 127.0
|
||||||
|
self.master_control.emit(str(target_id), scaled)
|
||||||
|
elif action == 'mixer_solo':
|
||||||
|
on = (value > 63) if not inv else (value < 64)
|
||||||
|
self.mixer_solo.emit(target_id, on)
|
||||||
|
elif action == 'mixer_pfl':
|
||||||
|
on = (value > 63) if not inv else (value < 64)
|
||||||
|
self.mixer_pfl.emit(target_id, on)
|
||||||
|
elif action == 'master_control':
|
||||||
|
scaled = (127 - value if inv else value) / 127.0
|
||||||
|
self.master_control.emit(str(target_id), 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)
|
||||||
|
|||||||
+211
-181
@@ -3,31 +3,21 @@
|
|||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSlider,
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSlider,
|
||||||
QPushButton, QFrame, QScrollArea, QDial, QSizePolicy,
|
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 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
|
||||||
|
|
||||||
|
|
||||||
STYLESHEET = """
|
|
||||||
QWidget {{
|
|
||||||
background-color: {bg};
|
|
||||||
color: {text};
|
|
||||||
font-family: Arial;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class VUMeter(QWidget):
|
class VUMeter(QWidget):
|
||||||
def __init__(self, parent=None, orientation=Qt.Vertical):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._value = 0.0
|
self._value = 0.0
|
||||||
self._orientation = orientation
|
self.setMinimumWidth(28)
|
||||||
self.setMinimumWidth(24 if orientation == Qt.Vertical else 100)
|
self.setMinimumHeight(80)
|
||||||
self.setMinimumHeight(100 if orientation == Qt.Vertical else 24)
|
|
||||||
|
|
||||||
def set_value(self, value: float):
|
def set_value(self, value: float):
|
||||||
self._value = max(0.0, min(1.0, value))
|
self._value = max(0.0, min(1.0, value))
|
||||||
@@ -37,26 +27,32 @@ class VUMeter(QWidget):
|
|||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
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:
|
if rect.width() < 1 or rect.height() < 1:
|
||||||
painter.end()
|
painter.end()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Background
|
painter.fillRect(rect, QColor("#080808"))
|
||||||
painter.fillRect(rect, QColor("#0a0a0a"))
|
|
||||||
|
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
|
val = self._value
|
||||||
if val < 1e-10:
|
if val < 1e-10:
|
||||||
painter.setPen(QPen(QColor("#222222"), 1))
|
|
||||||
painter.drawRect(rect)
|
|
||||||
painter.end()
|
painter.end()
|
||||||
return
|
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 = QLinearGradient(0, rect.bottom(), 0, rect.top())
|
||||||
gradient.setColorAt(0.0, QColor("#003300"))
|
gradient.setColorAt(0.0, QColor("#003300"))
|
||||||
gradient.setColorAt(0.5, QColor("#00cc00"))
|
gradient.setColorAt(0.5, QColor("#00cc00"))
|
||||||
@@ -64,14 +60,21 @@ class VUMeter(QWidget):
|
|||||||
gradient.setColorAt(0.9, QColor("#ff6600"))
|
gradient.setColorAt(0.9, QColor("#ff6600"))
|
||||||
gradient.setColorAt(1.0, QColor("#ff0000"))
|
gradient.setColorAt(1.0, QColor("#ff0000"))
|
||||||
|
|
||||||
fill_h = int(rect.height() * fill_pct)
|
db = -46 * (1 - val) if val < 1.0 else 3
|
||||||
painter.fillRect(
|
fill_pct = (db + 46) / 49.0 if db <= 0 else 1.0
|
||||||
rect.x(), rect.bottom() - fill_h,
|
fill_pct = max(0.0, min(1.0, fill_pct))
|
||||||
rect.width(), fill_h, gradient
|
|
||||||
)
|
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()
|
painter.end()
|
||||||
|
|
||||||
|
|
||||||
@@ -82,13 +85,13 @@ class ChannelStrip(QFrame):
|
|||||||
self.channel_index = channel_index
|
self.channel_index = channel_index
|
||||||
self._channel = engine.get_channel(channel_index)
|
self._channel = engine.get_channel(channel_index)
|
||||||
|
|
||||||
self.setFixedWidth(110)
|
self.setMinimumWidth(90)
|
||||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QFrame {
|
ChannelStrip {
|
||||||
background-color: #151515;
|
background-color: #181a1e;
|
||||||
border: 1px solid #333333;
|
border: 1px solid #282a30;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -105,109 +108,117 @@ class ChannelStrip(QFrame):
|
|||||||
root.setContentsMargins(4, 6, 4, 6)
|
root.setContentsMargins(4, 6, 4, 6)
|
||||||
root.setSpacing(4)
|
root.setSpacing(4)
|
||||||
|
|
||||||
# Name label
|
|
||||||
self.name_btn = QPushButton(self._channel.name)
|
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("""
|
self.name_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #aaaaaa;
|
color: #999aaa;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
font-size: 8pt;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: #222222;
|
background-color: #22252c;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
root.addWidget(self.name_btn)
|
root.addWidget(self.name_btn)
|
||||||
|
|
||||||
# VU Meter
|
self.vu_meter = VUMeter()
|
||||||
self.vu_meter = VUMeter(orientation=Qt.Vertical)
|
|
||||||
self.vu_meter.setMinimumHeight(120)
|
|
||||||
root.addWidget(self.vu_meter, stretch=1)
|
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 = QSlider(Qt.Vertical)
|
||||||
self.volume_slider.setRange(0, 100)
|
self.volume_slider.setRange(0, 100)
|
||||||
self.volume_slider.setValue(100)
|
self.volume_slider.setValue(100)
|
||||||
self.volume_slider.setFixedHeight(80)
|
self.volume_slider.setFixedHeight(60)
|
||||||
self.volume_slider.setStyleSheet("""
|
self.volume_slider.setStyleSheet("""
|
||||||
QSlider::groove:vertical {
|
QSlider::groove:vertical {
|
||||||
background: #222222;
|
background: #22242a;
|
||||||
width: 6px;
|
width: 4px;
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
QSlider::handle:vertical {
|
|
||||||
background: #666666;
|
|
||||||
height: 12px;
|
|
||||||
width: 16px;
|
|
||||||
margin: -4px -4px;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
QSlider::handle:vertical:hover {
|
QSlider::handle:vertical {
|
||||||
background: #aaaaaa;
|
background: #8890a0;
|
||||||
|
height: 12px;
|
||||||
|
width: 14px;
|
||||||
|
margin: -4px -5px;
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
QSlider::handle:vertical:hover { background: #b0b8c8; }
|
||||||
QSlider::sub-page:vertical {
|
QSlider::sub-page:vertical {
|
||||||
background: #00aa44;
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
border-radius: 3px;
|
stop:0 #00cc66, stop:1 #008844);
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
vol_row.addWidget(self.volume_slider, alignment=Qt.AlignCenter)
|
root.addWidget(self.volume_slider, alignment=Qt.AlignCenter)
|
||||||
root.addLayout(vol_row)
|
|
||||||
|
|
||||||
# Mute / Solo buttons
|
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
btn_row.setSpacing(4)
|
btn_row.setSpacing(3)
|
||||||
|
|
||||||
self.mute_btn = QPushButton()
|
self.mute_btn = QPushButton()
|
||||||
self.mute_btn.setFixedSize(32, 28)
|
self.mute_btn.setFixedSize(28, 24)
|
||||||
self.mute_btn.setIconSize(self.mute_btn.size() * 0.7)
|
self.mute_btn.setIconSize(QSize(16, 16))
|
||||||
self.mute_btn.setIcon(icon_mute())
|
self.mute_btn.setIcon(icon_mute())
|
||||||
self.mute_btn.setCheckable(True)
|
self.mute_btn.setCheckable(True)
|
||||||
|
self.mute_btn.setToolTip("Mute")
|
||||||
self.mute_btn.setStyleSheet("""
|
self.mute_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #2a1a1a;
|
background-color: #242026;
|
||||||
border: 1px solid #4a2a2a;
|
border: 1px solid #3a303a;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
QPushButton:checked {
|
QPushButton:checked {
|
||||||
background-color: #5a2020;
|
background-color: #4a1a1a;
|
||||||
border: 1px solid #ff4444;
|
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)
|
btn_row.addWidget(self.mute_btn)
|
||||||
|
|
||||||
self.solo_btn = QPushButton()
|
self.solo_btn = QPushButton()
|
||||||
self.solo_btn.setFixedSize(32, 28)
|
self.solo_btn.setFixedSize(28, 24)
|
||||||
self.solo_btn.setIconSize(self.solo_btn.size() * 0.7)
|
self.solo_btn.setIconSize(QSize(16, 16))
|
||||||
self.solo_btn.setIcon(icon_solo())
|
self.solo_btn.setIcon(icon_solo())
|
||||||
self.solo_btn.setCheckable(True)
|
self.solo_btn.setCheckable(True)
|
||||||
|
self.solo_btn.setToolTip("Solo")
|
||||||
self.solo_btn.setStyleSheet("""
|
self.solo_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #2a2a1a;
|
background-color: #262420;
|
||||||
border: 1px solid #4a4a2a;
|
border: 1px solid #3a3830;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
QPushButton:checked {
|
QPushButton:checked {
|
||||||
background-color: #5a5a20;
|
background-color: #4a4a1a;
|
||||||
border: 1px solid #ffaa00;
|
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)
|
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)
|
root.addLayout(btn_row)
|
||||||
|
|
||||||
def _connect_signals(self):
|
def _connect_signals(self):
|
||||||
@@ -215,11 +226,11 @@ class ChannelStrip(QFrame):
|
|||||||
self.volume_slider.valueChanged.connect(self._on_volume_changed)
|
self.volume_slider.valueChanged.connect(self._on_volume_changed)
|
||||||
self.mute_btn.toggled.connect(self._on_mute_toggled)
|
self.mute_btn.toggled.connect(self._on_mute_toggled)
|
||||||
self.solo_btn.toggled.connect(self._on_solo_toggled)
|
self.solo_btn.toggled.connect(self._on_solo_toggled)
|
||||||
|
self.pfl_btn.toggled.connect(self._on_pfl_toggled)
|
||||||
|
|
||||||
def _rename(self):
|
def _rename(self):
|
||||||
name, ok = QInputDialog.getText(
|
name, ok = QInputDialog.getText(
|
||||||
self, "Rename Channel",
|
self, "Rename Channel", "Channel name:",
|
||||||
"Channel name:",
|
|
||||||
text=self._channel.name
|
text=self._channel.name
|
||||||
)
|
)
|
||||||
if ok and name.strip():
|
if ok and name.strip():
|
||||||
@@ -235,6 +246,9 @@ class ChannelStrip(QFrame):
|
|||||||
def _on_solo_toggled(self, checked: bool):
|
def _on_solo_toggled(self, checked: bool):
|
||||||
self.engine.set_channel_solo(self.channel_index, checked)
|
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):
|
def _refresh(self):
|
||||||
self.vu_meter.set_value(self._channel.vu_peak)
|
self.vu_meter.set_value(self._channel.vu_peak)
|
||||||
|
|
||||||
@@ -248,13 +262,13 @@ class MasterSection(QFrame):
|
|||||||
self.engine = engine
|
self.engine = engine
|
||||||
self._master = engine.mixer['master']
|
self._master = engine.mixer['master']
|
||||||
|
|
||||||
self.setFixedWidth(180)
|
self.setMinimumWidth(180)
|
||||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QFrame {
|
MasterSection {
|
||||||
background-color: #151515;
|
background-color: #181a1e;
|
||||||
border: 1px solid #333333;
|
border: 1px solid #282a30;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -268,70 +282,72 @@ class MasterSection(QFrame):
|
|||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
root.setContentsMargins(6, 6, 6, 6)
|
root.setContentsMargins(8, 6, 8, 6)
|
||||||
root.setSpacing(6)
|
root.setSpacing(4)
|
||||||
|
|
||||||
# Header
|
|
||||||
header = QLabel("MASTER")
|
header = QLabel("MASTER")
|
||||||
header.setFont(QFont("Arial", 10, QFont.Bold))
|
header.setFont(QFont("Segoe UI", 10, QFont.Bold))
|
||||||
header.setAlignment(Qt.AlignCenter)
|
header.setAlignment(Qt.AlignCenter)
|
||||||
header.setStyleSheet("color: #00ff41; background: transparent; border: none;")
|
header.setStyleSheet("color: #00cc88; background: transparent; border: none;")
|
||||||
root.addWidget(header)
|
root.addWidget(header)
|
||||||
|
|
||||||
# VU Meter
|
self.vu_meter = VUMeter()
|
||||||
self.vu_meter = VUMeter(orientation=Qt.Vertical)
|
|
||||||
self.vu_meter.setMinimumHeight(100)
|
|
||||||
root.addWidget(self.vu_meter)
|
root.addWidget(self.vu_meter)
|
||||||
|
|
||||||
# LUFS readout
|
|
||||||
self.lufs_label = QLabel("-70.0 LUFS")
|
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.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)
|
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 = QFrame()
|
||||||
comp_frame.setStyleSheet("""
|
comp_frame.setStyleSheet("""
|
||||||
QFrame {
|
QFrame {
|
||||||
background-color: #111111;
|
background-color: #121318;
|
||||||
border: 1px solid #2a2a2a;
|
border: 1px solid #22242a;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
comp_layout = QVBoxLayout(comp_frame)
|
comp_layout = QVBoxLayout(comp_frame)
|
||||||
comp_layout.setContentsMargins(4, 4, 4, 4)
|
comp_layout.setContentsMargins(4, 4, 4, 4)
|
||||||
comp_layout.setSpacing(3)
|
comp_layout.setSpacing(2)
|
||||||
|
|
||||||
comp_header_row = QHBoxLayout()
|
comp_header = QHBoxLayout()
|
||||||
comp_label = QLabel("COMPRESSOR")
|
cl = QLabel("COMPRESSOR")
|
||||||
comp_label.setFont(QFont("Arial", 8, QFont.Bold))
|
cl.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||||
comp_label.setStyleSheet("color: #888888; background: transparent; border: none;")
|
cl.setStyleSheet("color: #778; background: transparent; border: none;")
|
||||||
comp_header_row.addWidget(comp_label)
|
comp_header.addWidget(cl)
|
||||||
|
|
||||||
self.comp_bypass = QPushButton("B")
|
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.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("""
|
self.comp_bypass.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #2a2a2a;
|
background-color: #22242a;
|
||||||
color: #888888;
|
color: #778;
|
||||||
border: 1px solid #444;
|
border: 1px solid #333;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
QPushButton:checked {
|
QPushButton:checked {
|
||||||
background-color: #5a2020;
|
background-color: #4a1a1a;
|
||||||
color: #ff6666;
|
color: #ff6666;
|
||||||
border: 1px solid #ff4444;
|
border: 1px solid #ff4444;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
self.comp_bypass.setToolTip("Bypass compressor")
|
self.comp_bypass.setToolTip("Bypass compressor")
|
||||||
comp_header_row.addWidget(self.comp_bypass)
|
comp_header.addWidget(self.comp_bypass)
|
||||||
comp_layout.addLayout(comp_header_row)
|
comp_layout.addLayout(comp_header)
|
||||||
|
|
||||||
knobs_row = QHBoxLayout()
|
knobs = QHBoxLayout()
|
||||||
knobs_row.setSpacing(2)
|
knobs.setSpacing(4)
|
||||||
for label, attr, default, lo, hi, suffix in [
|
for label, attr, default, lo, hi, suffix in [
|
||||||
("THR", "threshold_db", -20, -60, 0, "dB"),
|
("THR", "threshold_db", -20, -60, 0, "dB"),
|
||||||
("RAT", "ratio", 4, 1, 20, ":1"),
|
("RAT", "ratio", 4, 1, 20, ":1"),
|
||||||
@@ -339,60 +355,58 @@ 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, default, lo, hi, suffix)
|
k = self._make_knob(label, self._master.compressor, attr,
|
||||||
knobs_row.addWidget(k)
|
default, lo, hi, suffix)
|
||||||
comp_layout.addLayout(knobs_row)
|
knobs.addWidget(k)
|
||||||
|
comp_layout.addLayout(knobs)
|
||||||
root.addWidget(comp_frame)
|
root.addWidget(comp_frame)
|
||||||
|
|
||||||
# Limiter section
|
|
||||||
lim_frame = QFrame()
|
lim_frame = QFrame()
|
||||||
lim_frame.setStyleSheet("""
|
lim_frame.setStyleSheet("""
|
||||||
QFrame {
|
QFrame {
|
||||||
background-color: #111111;
|
background-color: #121318;
|
||||||
border: 1px solid #2a2a2a;
|
border: 1px solid #22242a;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
lim_layout = QVBoxLayout(lim_frame)
|
lim_layout = QVBoxLayout(lim_frame)
|
||||||
lim_layout.setContentsMargins(4, 4, 4, 4)
|
lim_layout.setContentsMargins(4, 4, 4, 4)
|
||||||
lim_layout.setSpacing(3)
|
lim_layout.setSpacing(2)
|
||||||
|
|
||||||
lim_header_row = QHBoxLayout()
|
lim_header = QHBoxLayout()
|
||||||
lim_label = QLabel("LIMITER")
|
ll = QLabel("LIMITER")
|
||||||
lim_label.setFont(QFont("Arial", 8, QFont.Bold))
|
ll.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||||
lim_label.setStyleSheet("color: #888888; background: transparent; border: none;")
|
ll.setStyleSheet("color: #778; background: transparent; border: none;")
|
||||||
lim_header_row.addWidget(lim_label)
|
lim_header.addWidget(ll)
|
||||||
|
|
||||||
self.lim_bypass = QPushButton("B")
|
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.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("""
|
self.lim_bypass.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #2a2a2a;
|
background-color: #22242a;
|
||||||
color: #888888;
|
color: #778;
|
||||||
border: 1px solid #444;
|
border: 1px solid #333;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
QPushButton:checked {
|
QPushButton:checked {
|
||||||
background-color: #5a2020;
|
background-color: #4a1a1a;
|
||||||
color: #ff6666;
|
color: #ff6666;
|
||||||
border: 1px solid #ff4444;
|
border: 1px solid #ff4444;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
self.lim_bypass.setToolTip("Bypass limiter")
|
self.lim_bypass.setToolTip("Bypass limiter")
|
||||||
lim_header_row.addWidget(self.lim_bypass)
|
lim_header.addWidget(self.lim_bypass)
|
||||||
lim_layout.addLayout(lim_header_row)
|
lim_layout.addLayout(lim_header)
|
||||||
|
|
||||||
lim_knobs = QHBoxLayout()
|
lim_knobs = QHBoxLayout()
|
||||||
lim_knobs.setSpacing(2)
|
lim_knobs.setSpacing(4)
|
||||||
k = self._make_knob("CEIL", self._master.limiter, "ceiling_db", -0.1, -10, 0, "dB")
|
k = self._make_knob("CEIL", self._master.limiter, "ceiling_db",
|
||||||
|
-0.1, -10, 0, "dB")
|
||||||
lim_knobs.addWidget(k)
|
lim_knobs.addWidget(k)
|
||||||
lim_layout.addLayout(lim_knobs)
|
lim_layout.addLayout(lim_knobs)
|
||||||
|
|
||||||
root.addWidget(lim_frame)
|
root.addWidget(lim_frame)
|
||||||
|
|
||||||
root.addStretch()
|
root.addStretch()
|
||||||
|
|
||||||
def _make_knob(self, label, obj, attr, default, lo, hi, suffix):
|
def _make_knob(self, label, obj, attr, default, lo, hi, suffix):
|
||||||
@@ -403,24 +417,23 @@ class MasterSection(QFrame):
|
|||||||
layout.setSpacing(1)
|
layout.setSpacing(1)
|
||||||
|
|
||||||
lbl = QLabel(label)
|
lbl = QLabel(label)
|
||||||
lbl.setFont(QFont("Arial", 7, QFont.Bold))
|
lbl.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||||
lbl.setAlignment(Qt.AlignCenter)
|
lbl.setAlignment(Qt.AlignCenter)
|
||||||
lbl.setStyleSheet("color: #666666; background: transparent; border: none;")
|
lbl.setStyleSheet("color: #556; background: transparent; border: none;")
|
||||||
layout.addWidget(lbl)
|
layout.addWidget(lbl)
|
||||||
|
|
||||||
dial = QDial()
|
dial = QDial()
|
||||||
dial.setFixedSize(32, 32)
|
dial.setFixedSize(44, 44)
|
||||||
dial.setRange(0, 1000)
|
dial.setRange(0, 1000)
|
||||||
dial.setNotchesVisible(False)
|
dial.setNotchesVisible(False)
|
||||||
dial.setStyleSheet("""
|
dial.setStyleSheet("""
|
||||||
QDial {
|
QDial {
|
||||||
background-color: #1a1a1a;
|
background-color: #121318;
|
||||||
border: 1px solid #333;
|
border: 1px solid #2a2c33;
|
||||||
border-radius: 16px;
|
border-radius: 22px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Map value to dial position
|
|
||||||
def _val_to_pos(v):
|
def _val_to_pos(v):
|
||||||
return int((v - lo) / (hi - lo) * 1000)
|
return int((v - lo) / (hi - lo) * 1000)
|
||||||
|
|
||||||
@@ -430,9 +443,9 @@ class MasterSection(QFrame):
|
|||||||
dial.setValue(_val_to_pos(default))
|
dial.setValue(_val_to_pos(default))
|
||||||
|
|
||||||
val_label = QLabel(f"{default}{suffix}")
|
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.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):
|
def _on_dial(v):
|
||||||
val = _pos_to_val(v)
|
val = _pos_to_val(v)
|
||||||
@@ -454,7 +467,6 @@ 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
|
||||||
|
|
||||||
def _connect_signals(self):
|
def _connect_signals(self):
|
||||||
@@ -468,6 +480,18 @@ class MasterSection(QFrame):
|
|||||||
def _refresh(self):
|
def _refresh(self):
|
||||||
self.vu_meter.set_value(self._master.vu_peak)
|
self.vu_meter.set_value(self._master.vu_peak)
|
||||||
self.lufs_label.setText(f"{self._master.lufs_current:.1f} LUFS")
|
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):
|
def apply_theme(self, theme: dict):
|
||||||
pass
|
pass
|
||||||
@@ -477,13 +501,11 @@ class MixerWidget(QFrame):
|
|||||||
def __init__(self, engine, parent=None):
|
def __init__(self, engine, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
|
|
||||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QFrame {
|
MixerWidget {
|
||||||
background-color: #0d0d0d;
|
background-color: #121318;
|
||||||
border: 2px solid #333333;
|
border: 1px solid #282a30;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -491,34 +513,36 @@ class MixerWidget(QFrame):
|
|||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
root.setContentsMargins(8, 8, 8, 8)
|
root.setContentsMargins(6, 4, 6, 6)
|
||||||
root.setSpacing(6)
|
root.setSpacing(4)
|
||||||
|
|
||||||
# Header
|
|
||||||
header = QLabel("MIXER")
|
header = QLabel("MIXER")
|
||||||
header.setFont(QFont("Arial", 10, QFont.Bold))
|
header.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
||||||
header.setStyleSheet("color: #666666; background: transparent; border: none;")
|
header.setStyleSheet("color: #556670; background: transparent; border: none; "
|
||||||
|
"letter-spacing: 2px;")
|
||||||
root.addWidget(header)
|
root.addWidget(header)
|
||||||
|
|
||||||
# Scrollable channel strips
|
|
||||||
scroll = QScrollArea()
|
scroll = QScrollArea()
|
||||||
scroll.setWidgetResizable(True)
|
scroll.setWidgetResizable(True)
|
||||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||||
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
scroll.setFixedHeight(350)
|
|
||||||
scroll.setStyleSheet("""
|
scroll.setStyleSheet("""
|
||||||
QScrollArea {
|
QScrollArea {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
QScrollBar:horizontal {
|
QScrollBar:horizontal {
|
||||||
background: #1a1a1a;
|
background: #1a1c22;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
QScrollBar::handle:horizontal {
|
QScrollBar::handle:horizontal {
|
||||||
background: #444444;
|
background: #3a3c44;
|
||||||
border-radius: 4px;
|
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)
|
self._strips.append(strip)
|
||||||
channels_layout.addWidget(strip)
|
channels_layout.addWidget(strip)
|
||||||
|
|
||||||
# Master section
|
|
||||||
self.master_section = MasterSection(self.engine)
|
self.master_section = MasterSection(self.engine)
|
||||||
channels_layout.addWidget(self.master_section)
|
channels_layout.addWidget(self.master_section)
|
||||||
|
|
||||||
channels_layout.addStretch()
|
|
||||||
|
|
||||||
scroll.setWidget(scroll_content)
|
scroll.setWidget(scroll_content)
|
||||||
root.addWidget(scroll)
|
root.addWidget(scroll)
|
||||||
|
|
||||||
@@ -547,3 +568,12 @@ class MixerWidget(QFrame):
|
|||||||
for strip in self._strips:
|
for strip in self._strips:
|
||||||
strip.apply_theme(theme)
|
strip.apply_theme(theme)
|
||||||
self.master_section.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)
|
||||||
+381
-215
@@ -1,211 +1,61 @@
|
|||||||
# top_bar_widget.py
|
# top_bar_widget.py
|
||||||
|
|
||||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton
|
from PySide6.QtWidgets import (
|
||||||
from PySide6.QtCore import Qt, QTimer, Signal, QSize
|
QFrame, QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
|
||||||
|
QComboBox, QScrollArea, QWidget
|
||||||
|
)
|
||||||
|
from PySide6.QtCore import Qt, QTimer, Signal, QSize, QPoint
|
||||||
from PySide6.QtGui import QFont
|
from PySide6.QtGui import QFont
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from icons import icon_theme
|
from icons import icon_theme, icon_cog, icon_save, icon_load
|
||||||
|
|
||||||
from icons import icon_theme
|
|
||||||
|
|
||||||
|
|
||||||
THEMES = [
|
THEMES = [
|
||||||
# Dark themes
|
{'name': 'DARK TEAL', 'accent': '#00b894', 'accent_dim': '#00876a', 'bg': '#121418', 'bg2': '#1a1c23', 'border': '#2a2c35', 'text': '#d0d4dc', 'is_light': False},
|
||||||
{
|
{'name': 'GREEN', 'accent': '#00ff41', 'accent_dim': '#00aa2a', 'bg': '#0d0d0d', 'bg2': '#111111', 'border': '#1a3a1a', 'text': '#cccccc', 'is_light': False},
|
||||||
'name': 'GREEN',
|
{'name': 'AMBER', 'accent': '#ffaa00', 'accent_dim': '#cc8800', 'bg': '#0d0d00', 'bg2': '#111100', 'border': '#3a3000', 'text': '#ddddcc', 'is_light': False},
|
||||||
'accent': '#00ff41',
|
{'name': 'BLUE', 'accent': '#00aaff', 'accent_dim': '#0077cc', 'bg': '#00080d', 'bg2': '#001122', 'border': '#001a3a', 'text': '#ccd8dd', 'is_light': False},
|
||||||
'accent_dim': '#00aa2a',
|
{'name': 'RED', 'accent': '#ff4444', 'accent_dim': '#cc2222', 'bg': '#0d0000', 'bg2': '#110000', 'border': '#3a0000', 'text': '#ddcccc', 'is_light': False},
|
||||||
'bg': '#0d0d0d',
|
{'name': 'PURPLE', 'accent': '#cc00ff', 'accent_dim': '#9900cc', 'bg': '#0d000d', 'bg2': '#1a001a', 'border': '#3a003a', 'text': '#ddccdd', 'is_light': False},
|
||||||
'bg2': '#111111',
|
{'name': 'CYAN', 'accent': '#00ffff', 'accent_dim': '#00cccc', 'bg': '#000d0d', 'bg2': '#001a1a', 'border': '#003a3a', 'text': '#ccdddd', 'is_light': False},
|
||||||
'border': '#1a3a1a',
|
{'name': 'ORANGE', 'accent': '#ff8800', 'accent_dim': '#cc6600', 'bg': '#0d0700', 'bg2': '#1a1100', 'border': '#3a2200', 'text': '#ddccbb', 'is_light': False},
|
||||||
'text': '#cccccc',
|
{'name': 'PINK', 'accent': '#ff1493', 'accent_dim': '#cc0066', 'bg': '#0d0005', 'bg2': '#1a000a', 'border': '#3a0015', 'text': '#ddccdd', 'is_light': False},
|
||||||
'is_light': False,
|
{'name': 'LIME', 'accent': '#ccff00', 'accent_dim': '#99cc00', 'bg': '#0a0d00', 'bg2': '#111a00', 'border': '#223a00', 'text': '#ddddcc', 'is_light': False},
|
||||||
},
|
{'name': 'GOLD', 'accent': '#ffd700', 'accent_dim': '#ccaa00', 'bg': '#0d0d05', 'bg2': '#1a1a0a', 'border': '#3a3a15', 'text': '#ddddbb', 'is_light': False},
|
||||||
{
|
{'name': 'VIOLET', 'accent': '#8a2be2', 'accent_dim': '#6600cc', 'bg': '#05000d', 'bg2': '#0a0015', 'border': '#15002a', 'text': '#ccbbdd', 'is_light': False},
|
||||||
'name': 'AMBER',
|
{'name': 'TURQUOISE', 'accent': '#40e0d0', 'accent_dim': '#20b0a0', 'bg': '#000d0a', 'bg2': '#001a15', 'border': '#00382d', 'text': '#ccddda', 'is_light': False},
|
||||||
'accent': '#ffaa00',
|
{'name': 'CRIMSON', 'accent': '#dc143c', 'accent_dim': '#aa0022', 'bg': '#0d0002', 'bg2': '#1a0005', 'border': '#3a000a', 'text': '#ddcccc', 'is_light': False},
|
||||||
'accent_dim': '#cc8800',
|
{'name': 'CHARTREUSE', 'accent': '#7fff00', 'accent_dim': '#5fcc00', 'bg': '#050d00', 'bg2': '#0a1a00', 'border': '#153a00', 'text': '#ddddcc', 'is_light': False},
|
||||||
'bg': '#0d0d00',
|
{'name': 'PAPER WHITE', 'accent': '#0066cc', 'accent_dim': '#004488', 'bg': '#ffffff', 'bg2': '#eeeeee', 'border': '#cccccc', 'text': '#111111', 'is_light': True},
|
||||||
'bg2': '#111100',
|
{'name': 'GREY PAPER', 'accent': '#cc3300', 'accent_dim': '#992200', 'bg': '#e8e8e8', 'bg2': '#d8d8d8', 'border': '#aaaaaa', 'text': '#111111', 'is_light': True},
|
||||||
'border': '#3a3000',
|
{'name': 'CREAM', 'accent': '#884422', 'accent_dim': '#663311', 'bg': '#f5f0e8', 'bg2': '#e8e0d4', 'border': '#c0b8a8', 'text': '#221100', 'is_light': True},
|
||||||
'text': '#ddddcc',
|
{'name': 'FROST', 'accent': '#0088aa', 'accent_dim': '#006688', 'bg': '#f0f4f8', 'bg2': '#e0e8f0', 'border': '#b0c0d0', 'text': '#001122', 'is_light': True},
|
||||||
'is_light': False,
|
{'name': 'LIGHT LAVENDER','accent': '#6633cc','accent_dim': '#4422aa', 'bg': '#f5f0fa', 'bg2': '#e8e0f0', 'border': '#c0b8d0', 'text': '#110022', 'is_light': True},
|
||||||
},
|
]
|
||||||
{
|
|
||||||
'name': 'BLUE',
|
RATIOS = [
|
||||||
'accent': '#00aaff',
|
("21:9", 2560, 1080),
|
||||||
'accent_dim': '#0077cc',
|
("16:9", 1920, 1080),
|
||||||
'bg': '#00080d',
|
("4:3", 1440, 1080),
|
||||||
'bg2': '#001122',
|
|
||||||
'border': '#001a3a',
|
|
||||||
'text': '#ccd8dd',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'RED',
|
|
||||||
'accent': '#ff4444',
|
|
||||||
'accent_dim': '#cc2222',
|
|
||||||
'bg': '#0d0000',
|
|
||||||
'bg2': '#110000',
|
|
||||||
'border': '#3a0000',
|
|
||||||
'text': '#ddcccc',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'PURPLE',
|
|
||||||
'accent': '#cc00ff',
|
|
||||||
'accent_dim': '#9900cc',
|
|
||||||
'bg': '#0d000d',
|
|
||||||
'bg2': '#1a001a',
|
|
||||||
'border': '#3a003a',
|
|
||||||
'text': '#ddccdd',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'CYAN',
|
|
||||||
'accent': '#00ffff',
|
|
||||||
'accent_dim': '#00cccc',
|
|
||||||
'bg': '#000d0d',
|
|
||||||
'bg2': '#001a1a',
|
|
||||||
'border': '#003a3a',
|
|
||||||
'text': '#ccdddd',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'ORANGE',
|
|
||||||
'accent': '#ff8800',
|
|
||||||
'accent_dim': '#cc6600',
|
|
||||||
'bg': '#0d0700',
|
|
||||||
'bg2': '#1a1100',
|
|
||||||
'border': '#3a2200',
|
|
||||||
'text': '#ddccbb',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'PINK',
|
|
||||||
'accent': '#ff1493',
|
|
||||||
'accent_dim': '#cc0066',
|
|
||||||
'bg': '#0d0005',
|
|
||||||
'bg2': '#1a000a',
|
|
||||||
'border': '#3a0015',
|
|
||||||
'text': '#ddccdd',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'LIME',
|
|
||||||
'accent': '#ccff00',
|
|
||||||
'accent_dim': '#99cc00',
|
|
||||||
'bg': '#0a0d00',
|
|
||||||
'bg2': '#111a00',
|
|
||||||
'border': '#223a00',
|
|
||||||
'text': '#ddddcc',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'GOLD',
|
|
||||||
'accent': '#ffd700',
|
|
||||||
'accent_dim': '#ccaa00',
|
|
||||||
'bg': '#0d0d05',
|
|
||||||
'bg2': '#1a1a0a',
|
|
||||||
'border': '#3a3a15',
|
|
||||||
'text': '#ddddbb',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'VIOLET',
|
|
||||||
'accent': '#8a2be2',
|
|
||||||
'accent_dim': '#6600cc',
|
|
||||||
'bg': '#05000d',
|
|
||||||
'bg2': '#0a0015',
|
|
||||||
'border': '#15002a',
|
|
||||||
'text': '#ccbbdd',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'TURQUOISE',
|
|
||||||
'accent': '#40e0d0',
|
|
||||||
'accent_dim': '#20b0a0',
|
|
||||||
'bg': '#000d0a',
|
|
||||||
'bg2': '#001a15',
|
|
||||||
'border': '#00382d',
|
|
||||||
'text': '#ccddda',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'CRIMSON',
|
|
||||||
'accent': '#dc143c',
|
|
||||||
'accent_dim': '#aa0022',
|
|
||||||
'bg': '#0d0002',
|
|
||||||
'bg2': '#1a0005',
|
|
||||||
'border': '#3a000a',
|
|
||||||
'text': '#ddcccc',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'CHARTREUSE',
|
|
||||||
'accent': '#7fff00',
|
|
||||||
'accent_dim': '#5fcc00',
|
|
||||||
'bg': '#050d00',
|
|
||||||
'bg2': '#0a1a00',
|
|
||||||
'border': '#153a00',
|
|
||||||
'text': '#ddddcc',
|
|
||||||
'is_light': False,
|
|
||||||
},
|
|
||||||
# Light themes
|
|
||||||
{
|
|
||||||
'name': 'LIGHT BLUE',
|
|
||||||
'accent': '#0066cc',
|
|
||||||
'accent_dim': '#0044aa',
|
|
||||||
'bg': '#f5f5f5',
|
|
||||||
'bg2': '#e8e8e8',
|
|
||||||
'border': '#bbbbbb',
|
|
||||||
'text': '#222222',
|
|
||||||
'is_light': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'LIGHT GREEN',
|
|
||||||
'accent': '#00aa44',
|
|
||||||
'accent_dim': '#008833',
|
|
||||||
'bg': '#f0f5f0',
|
|
||||||
'bg2': '#e0e8e0',
|
|
||||||
'border': '#aaccaa',
|
|
||||||
'text': '#112211',
|
|
||||||
'is_light': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'LIGHT AMBER',
|
|
||||||
'accent': '#cc8800',
|
|
||||||
'accent_dim': '#aa6600',
|
|
||||||
'bg': '#f5f3f0',
|
|
||||||
'bg2': '#e8e5e0',
|
|
||||||
'border': '#ccbb99',
|
|
||||||
'text': '#221100',
|
|
||||||
'is_light': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'LIGHT PURPLE',
|
|
||||||
'accent': '#8844cc',
|
|
||||||
'accent_dim': '#6622aa',
|
|
||||||
'bg': '#f3f0f5',
|
|
||||||
'bg2': '#e5e0e8',
|
|
||||||
'border': '#ccbbdd',
|
|
||||||
'text': '#221122',
|
|
||||||
'is_light': True,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TopBarWidget(QFrame):
|
class TopBarWidget(QFrame):
|
||||||
theme_changed = Signal(dict)
|
theme_changed = Signal(dict)
|
||||||
|
ratio_changed = Signal(int, int)
|
||||||
|
save_session = Signal()
|
||||||
|
load_session = Signal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._theme_index = 0
|
self._theme_index = 0
|
||||||
|
self._ratio_index = 1
|
||||||
|
self._midi_engine = None
|
||||||
|
|
||||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
||||||
self.setFixedHeight(80)
|
self.setFixedHeight(56)
|
||||||
self._apply_bar_style()
|
self._apply_bar_style()
|
||||||
|
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
@@ -214,13 +64,15 @@ class TopBarWidget(QFrame):
|
|||||||
self.timer.setInterval(1000)
|
self.timer.setInterval(1000)
|
||||||
self.timer.timeout.connect(self._update)
|
self.timer.timeout.connect(self._update)
|
||||||
self.timer.start()
|
self.timer.start()
|
||||||
|
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
|
def set_midi_engine(self, engine):
|
||||||
|
self._midi_engine = engine
|
||||||
|
|
||||||
def _apply_bar_style(self):
|
def _apply_bar_style(self):
|
||||||
theme = THEMES[self._theme_index]
|
theme = THEMES[self._theme_index]
|
||||||
self.setStyleSheet(f"""
|
self.setStyleSheet(f"""
|
||||||
QFrame {{
|
TopBarWidget {{
|
||||||
background-color: {theme['bg2']};
|
background-color: {theme['bg2']};
|
||||||
border: 2px solid {theme['border']};
|
border: 2px solid {theme['border']};
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -229,46 +81,68 @@ class TopBarWidget(QFrame):
|
|||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
layout = QHBoxLayout(self)
|
layout = QHBoxLayout(self)
|
||||||
layout.setContentsMargins(20, 8, 20, 8)
|
layout.setContentsMargins(16, 4, 16, 4)
|
||||||
layout.setSpacing(12)
|
layout.setSpacing(8)
|
||||||
|
|
||||||
# Theme cycle button
|
|
||||||
self.theme_btn = QPushButton()
|
self.theme_btn = QPushButton()
|
||||||
self.theme_btn.setFixedSize(40, 40)
|
self.theme_btn.setFixedSize(30, 30)
|
||||||
self.theme_btn.setIconSize(QSize(22, 22))
|
self.theme_btn.setIconSize(QSize(16, 16))
|
||||||
self.theme_btn.setIcon(icon_theme())
|
self.theme_btn.setIcon(icon_theme())
|
||||||
self.theme_btn.setToolTip("Cycle colour theme")
|
self.theme_btn.setToolTip("Cycle colour theme")
|
||||||
self.theme_btn.clicked.connect(self._cycle_theme)
|
self.theme_btn.clicked.connect(self._cycle_theme)
|
||||||
self._style_theme_btn()
|
self._style_theme_btn()
|
||||||
layout.addWidget(self.theme_btn)
|
layout.addWidget(self.theme_btn)
|
||||||
|
|
||||||
# Station name
|
|
||||||
self.station_label = QLabel("GETTING TO IT")
|
self.station_label = QLabel("GETTING TO IT")
|
||||||
self.station_label.setFont(QFont("Arial", 16, QFont.Bold))
|
self.station_label.setFont(QFont("Arial", 13, QFont.Bold))
|
||||||
self.station_label.setStyleSheet(
|
self.station_label.setStyleSheet("color: #555555; background: transparent; border: none;")
|
||||||
"color: #444444; background: transparent; border: none;"
|
|
||||||
)
|
|
||||||
self.station_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
||||||
layout.addWidget(self.station_label)
|
layout.addWidget(self.station_label)
|
||||||
|
|
||||||
|
self.settings_btn = QPushButton()
|
||||||
|
self.settings_btn.setFixedSize(24, 24)
|
||||||
|
self.settings_btn.setIconSize(QSize(14, 14))
|
||||||
|
self.settings_btn.setIcon(icon_cog("#666666"))
|
||||||
|
self.settings_btn.setToolTip("Settings")
|
||||||
|
self.settings_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #3a3c44;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #2a2c34; border-color: #5a5c64; }
|
||||||
|
""")
|
||||||
|
self.settings_btn.clicked.connect(self._toggle_settings_popup)
|
||||||
|
layout.addWidget(self.settings_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
# Date
|
|
||||||
self.date_label = QLabel("")
|
self.date_label = QLabel("")
|
||||||
self.date_label.setFont(QFont("Arial", 20, QFont.Bold))
|
self.date_label.setFont(QFont("Arial", 13, QFont.Bold))
|
||||||
self.date_label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
self.date_label.setAlignment(Qt.AlignCenter)
|
||||||
layout.addWidget(self.date_label)
|
layout.addWidget(self.date_label)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
# Clock
|
|
||||||
self.clock_label = QLabel("")
|
self.clock_label = QLabel("")
|
||||||
self.clock_label.setFont(QFont("Courier New", 42, QFont.Bold))
|
self.clock_label.setFont(QFont("Courier New", 28, QFont.Bold))
|
||||||
self.clock_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
self.clock_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
self.clock_label.setMinimumWidth(220)
|
self.clock_label.setMinimumWidth(170)
|
||||||
self._style_clock_and_date()
|
self._style_clock_and_date()
|
||||||
layout.addWidget(self.clock_label)
|
layout.addWidget(self.clock_label)
|
||||||
|
|
||||||
|
# ── Settings popup ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _toggle_settings_popup(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.load_session.connect(self.load_session.emit)
|
||||||
|
pos = self.settings_btn.mapToGlobal(QPoint(0, self.settings_btn.height() + 4))
|
||||||
|
self._popup.move(pos)
|
||||||
|
self._popup.show()
|
||||||
|
|
||||||
|
# ── Theme ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _style_theme_btn(self):
|
def _style_theme_btn(self):
|
||||||
theme = THEMES[self._theme_index]
|
theme = THEMES[self._theme_index]
|
||||||
self.theme_btn.setStyleSheet(f"""
|
self.theme_btn.setStyleSheet(f"""
|
||||||
@@ -276,15 +150,12 @@ class TopBarWidget(QFrame):
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: {theme['accent']};
|
color: {theme['accent']};
|
||||||
border: 2px solid {theme['accent']};
|
border: 2px solid {theme['accent']};
|
||||||
border-radius: 20px;
|
border-radius: 15px;
|
||||||
}}
|
}}
|
||||||
QPushButton:hover {{
|
QPushButton:hover {{
|
||||||
background-color: {theme['accent']};
|
background-color: {theme['accent']};
|
||||||
color: {'#ffffff' if not theme['is_light'] else '#000000'};
|
color: {'#ffffff' if not theme['is_light'] else '#000000'};
|
||||||
}}
|
}}
|
||||||
QPushButton:pressed {{
|
|
||||||
background-color: {theme['accent_dim']};
|
|
||||||
}}
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def _style_clock_and_date(self):
|
def _style_clock_and_date(self):
|
||||||
@@ -292,7 +163,6 @@ class TopBarWidget(QFrame):
|
|||||||
self.clock_label.setStyleSheet(
|
self.clock_label.setStyleSheet(
|
||||||
f"color: {theme['accent']}; background: transparent; border: none;"
|
f"color: {theme['accent']}; background: transparent; border: none;"
|
||||||
)
|
)
|
||||||
# Date color adapts to light/dark
|
|
||||||
date_color = '#555555' if theme['is_light'] else '#aaaaaa'
|
date_color = '#555555' if theme['is_light'] else '#aaaaaa'
|
||||||
self.date_label.setStyleSheet(
|
self.date_label.setStyleSheet(
|
||||||
f"color: {date_color}; background: transparent; border: none;"
|
f"color: {date_color}; background: transparent; border: none;"
|
||||||
@@ -308,8 +178,304 @@ class TopBarWidget(QFrame):
|
|||||||
def _update(self):
|
def _update(self):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
self.clock_label.setText(now.strftime("%H:%M:%S"))
|
self.clock_label.setText(now.strftime("%H:%M:%S"))
|
||||||
self.date_label.setText(now.strftime("%A %d %B %Y"))
|
self.date_label.setText(now.strftime("%a %d %b"))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_theme(self) -> dict:
|
def current_theme(self) -> dict:
|
||||||
return THEMES[self._theme_index]
|
return THEMES[self._theme_index]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Settings Popup ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
POPUP_STYLE = """
|
||||||
|
QFrame#settingsPopup {
|
||||||
|
background-color: #1a1c23;
|
||||||
|
border: 1px solid #2a2c35;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
QPushButton#popupBtn {
|
||||||
|
background-color: #22242a;
|
||||||
|
color: #d0d4dc;
|
||||||
|
border: 1px solid #3a3c44;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 7pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton#popupBtn:hover {
|
||||||
|
background-color: #2a2c34;
|
||||||
|
border-color: #5a5c64;
|
||||||
|
}
|
||||||
|
QPushButton#ratioBtn {
|
||||||
|
background-color: #22242a;
|
||||||
|
color: #8890a0;
|
||||||
|
border: 1px solid #3a3c44;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
font-size: 7pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton#ratioBtn:hover {
|
||||||
|
background-color: #2a2c34;
|
||||||
|
border-color: #5a5c64;
|
||||||
|
}
|
||||||
|
QComboBox#portCombo {
|
||||||
|
background-color: #22242a;
|
||||||
|
color: #d0d4dc;
|
||||||
|
border: 1px solid #3a3c44;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 7pt;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
QComboBox#portCombo:hover { border-color: #5a5c64; }
|
||||||
|
QComboBox#portCombo::drop-down { border: none; width: 14px; }
|
||||||
|
QComboBox#portCombo QAbstractItemView {
|
||||||
|
background-color: #1a1c23;
|
||||||
|
color: #d0d4dc;
|
||||||
|
selection-background-color: #2a2c35;
|
||||||
|
border: 1px solid #3a3c44;
|
||||||
|
}
|
||||||
|
QScrollArea { border: none; background: transparent; }
|
||||||
|
QScrollBar:vertical { background: #1a1c22; width: 4px; border-radius: 2px; }
|
||||||
|
QScrollBar::handle:vertical { background: #3a3c44; border-radius: 2px; }
|
||||||
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsPopup(QFrame):
|
||||||
|
ratio_changed = Signal(int, int)
|
||||||
|
save_session = Signal()
|
||||||
|
load_session = Signal()
|
||||||
|
|
||||||
|
def __init__(self, midi_engine, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._midi_engine = midi_engine
|
||||||
|
self._ratio_index = 1
|
||||||
|
|
||||||
|
self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
|
||||||
|
self.setObjectName("settingsPopup")
|
||||||
|
self.setStyleSheet(POPUP_STYLE)
|
||||||
|
self.setFixedWidth(400)
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
root = QVBoxLayout(self)
|
||||||
|
root.setContentsMargins(12, 10, 12, 10)
|
||||||
|
root.setSpacing(6)
|
||||||
|
|
||||||
|
# Row 1: Aspect ratio + Save/Load
|
||||||
|
row1 = QHBoxLayout()
|
||||||
|
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.setObjectName("popupBtn")
|
||||||
|
save_btn.setFixedHeight(24)
|
||||||
|
save_btn.clicked.connect(self.save_session.emit)
|
||||||
|
row1.addWidget(save_btn)
|
||||||
|
|
||||||
|
load_btn = QPushButton("LOAD")
|
||||||
|
load_btn.setObjectName("popupBtn")
|
||||||
|
load_btn.setFixedHeight(24)
|
||||||
|
load_btn.clicked.connect(self.load_session.emit)
|
||||||
|
row1.addWidget(load_btn)
|
||||||
|
|
||||||
|
root.addLayout(row1)
|
||||||
|
|
||||||
|
# Row 2: MIDI port
|
||||||
|
row2 = QHBoxLayout()
|
||||||
|
row2.setSpacing(4)
|
||||||
|
self._port_combo = QComboBox()
|
||||||
|
self._port_combo.setObjectName("portCombo")
|
||||||
|
self._port_combo.setMinimumWidth(160)
|
||||||
|
row2.addWidget(self._port_combo)
|
||||||
|
|
||||||
|
refresh_btn = QPushButton("⟳")
|
||||||
|
refresh_btn.setObjectName("popupBtn")
|
||||||
|
refresh_btn.setFixedSize(26, 22)
|
||||||
|
refresh_btn.clicked.connect(self._refresh_ports)
|
||||||
|
row2.addWidget(refresh_btn)
|
||||||
|
|
||||||
|
self._connect_btn = QPushButton("CONNECT")
|
||||||
|
self._connect_btn.setObjectName("popupBtn")
|
||||||
|
self._connect_btn.setFixedHeight(22)
|
||||||
|
self._connect_btn.clicked.connect(self._connect_midi)
|
||||||
|
row2.addWidget(self._connect_btn)
|
||||||
|
|
||||||
|
self._midi_status = QLabel("")
|
||||||
|
self._midi_status.setStyleSheet("color: #888; background: transparent; border: none; font-size: 7pt;")
|
||||||
|
row2.addWidget(self._midi_status, stretch=1)
|
||||||
|
root.addLayout(row2)
|
||||||
|
|
||||||
|
# Row 3: MIDI learn scroll
|
||||||
|
learn_label = QLabel("MIDI LEARN")
|
||||||
|
learn_label.setStyleSheet("color: #00b894; background: transparent; border: none; font-size: 7pt; font-weight: bold; padding-top: 2px;")
|
||||||
|
root.addWidget(learn_label)
|
||||||
|
|
||||||
|
scroll = QScrollArea()
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
|
scroll.setFixedHeight(170)
|
||||||
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
scroll.setStyleSheet(POPUP_STYLE)
|
||||||
|
|
||||||
|
content = QWidget()
|
||||||
|
content.setStyleSheet("background: transparent;")
|
||||||
|
grid = QVBoxLayout(content)
|
||||||
|
grid.setContentsMargins(0, 0, 0, 0)
|
||||||
|
grid.setSpacing(2)
|
||||||
|
|
||||||
|
learn_actions = [
|
||||||
|
("Deck 1 Play/Pause", "deck_play_pause", "deck1"),
|
||||||
|
("Deck 1 Cue", "deck_cue", "deck1"),
|
||||||
|
("Deck 2 Play/Pause", "deck_play_pause", "deck2"),
|
||||||
|
("Deck 2 Cue", "deck_cue", "deck2"),
|
||||||
|
("Deck 3 Play/Pause", "deck_play_pause", "deck3"),
|
||||||
|
("Deck 4 Play/Pause", "deck_play_pause", "deck4"),
|
||||||
|
("Cart 1", "cart_trigger", 0),
|
||||||
|
("Cart 2", "cart_trigger", 1),
|
||||||
|
("Cart 3", "cart_trigger", 2),
|
||||||
|
("Cart 4", "cart_trigger", 3),
|
||||||
|
("Ch 1 Solo", "mixer_solo", 0),
|
||||||
|
("Ch 1 PFL", "mixer_pfl", 0),
|
||||||
|
("Ch 2 Solo", "mixer_solo", 1),
|
||||||
|
("Ch 2 PFL", "mixer_pfl", 1),
|
||||||
|
("Ch 3 Solo", "mixer_solo", 2),
|
||||||
|
("Ch 3 PFL", "mixer_pfl", 2),
|
||||||
|
("Ch 4 Solo", "mixer_solo", 3),
|
||||||
|
("Ch 4 PFL", "mixer_pfl", 3),
|
||||||
|
("Master THR", "master_control", "threshold_db"),
|
||||||
|
("Master RAT", "master_control", "ratio"),
|
||||||
|
("Master ATK", "master_control", "attack_ms"),
|
||||||
|
("Master REL", "master_control", "release_ms"),
|
||||||
|
("Master MKG", "master_control", "makeup_gain_db"),
|
||||||
|
("Master CEIL", "master_control", "ceiling_db"),
|
||||||
|
]
|
||||||
|
|
||||||
|
self._learn_btns = {}
|
||||||
|
for label, action, target_id in learn_actions:
|
||||||
|
row = QHBoxLayout()
|
||||||
|
row.setSpacing(3)
|
||||||
|
|
||||||
|
lbl = QLabel(label)
|
||||||
|
lbl.setStyleSheet("color: #d0d4dc; background: transparent; border: none; font-size: 7pt;")
|
||||||
|
row.addWidget(lbl, stretch=1)
|
||||||
|
|
||||||
|
btn = QPushButton("LEARN")
|
||||||
|
btn.setObjectName("popupBtn")
|
||||||
|
btn.setFixedSize(52, 20)
|
||||||
|
btn.setCheckable(True)
|
||||||
|
btn.toggled.connect(partial(self._toggle_learn, btn, action, target_id))
|
||||||
|
row.addWidget(btn)
|
||||||
|
self._learn_btns[(action, target_id)] = btn
|
||||||
|
|
||||||
|
clear_btn = QPushButton("X")
|
||||||
|
clear_btn.setObjectName("popupBtn")
|
||||||
|
clear_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #3a1a1a;
|
||||||
|
color: #ff6666;
|
||||||
|
border: 1px solid #5a2a2a;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 7pt;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #5a2a2a; }
|
||||||
|
""")
|
||||||
|
clear_btn.setFixedSize(20, 20)
|
||||||
|
clear_btn.clicked.connect(partial(self._clear_mapping, action, target_id))
|
||||||
|
row.addWidget(clear_btn)
|
||||||
|
|
||||||
|
grid.addLayout(row)
|
||||||
|
|
||||||
|
scroll.setWidget(content)
|
||||||
|
root.addWidget(scroll)
|
||||||
|
|
||||||
|
self._refresh_ports()
|
||||||
|
|
||||||
|
if self._midi_engine:
|
||||||
|
self._midi_engine.mapping_learned.connect(self._on_mapping_learned)
|
||||||
|
|
||||||
|
# ── Ratio ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _set_ratio(self, index):
|
||||||
|
self._ratio_index = index
|
||||||
|
for i, btn in enumerate(self._ratio_btns):
|
||||||
|
btn.setChecked(i == index)
|
||||||
|
_, w, h = RATIOS[index]
|
||||||
|
self.ratio_changed.emit(w, h)
|
||||||
|
|
||||||
|
# ── MIDI port ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _refresh_ports(self):
|
||||||
|
self._port_combo.clear()
|
||||||
|
if not self._midi_engine:
|
||||||
|
return
|
||||||
|
ports = self._midi_engine.list_ports()
|
||||||
|
if ports:
|
||||||
|
for p in ports:
|
||||||
|
self._port_combo.addItem(p)
|
||||||
|
self._connect_btn.setEnabled(True)
|
||||||
|
self._midi_status.setText("Select port")
|
||||||
|
else:
|
||||||
|
self._port_combo.addItem("No MIDI ports found")
|
||||||
|
self._connect_btn.setEnabled(False)
|
||||||
|
self._midi_status.setText("No MIDI devices")
|
||||||
|
|
||||||
|
def _connect_midi(self):
|
||||||
|
if not self._midi_engine or self._port_combo.currentText() == "No MIDI ports found":
|
||||||
|
return
|
||||||
|
self._midi_engine.stop()
|
||||||
|
idx = self._port_combo.currentIndex()
|
||||||
|
success = self._midi_engine.start(port_index=idx)
|
||||||
|
if success:
|
||||||
|
self._midi_status.setText(f"Connected")
|
||||||
|
self._midi_status.setStyleSheet("color: #00b894; background: transparent; border: none; font-size: 7pt;")
|
||||||
|
else:
|
||||||
|
self._midi_status.setText("Failed")
|
||||||
|
self._midi_status.setStyleSheet("color: #ff6666; background: transparent; border: none; font-size: 7pt;")
|
||||||
|
|
||||||
|
# ── MIDI learn ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _toggle_learn(self, btn, action, target_id, checked):
|
||||||
|
if not self._midi_engine:
|
||||||
|
btn.setChecked(False)
|
||||||
|
return
|
||||||
|
if checked:
|
||||||
|
self._midi_engine.start_learn(action, target_id)
|
||||||
|
for other_btn in self._learn_btns.values():
|
||||||
|
if other_btn is not btn:
|
||||||
|
other_btn.setChecked(False)
|
||||||
|
btn.setText("...")
|
||||||
|
else:
|
||||||
|
self._midi_engine.stop_learn()
|
||||||
|
btn.setText("LEARN")
|
||||||
|
|
||||||
|
def _clear_mapping(self, action, target_id):
|
||||||
|
if not self._midi_engine:
|
||||||
|
return
|
||||||
|
to_remove = []
|
||||||
|
for key, val in self._midi_engine.mapping.bindings.items():
|
||||||
|
if val['action'] == action and val['target_id'] == target_id:
|
||||||
|
to_remove.append(key)
|
||||||
|
for key in to_remove:
|
||||||
|
del self._midi_engine.mapping.bindings[key]
|
||||||
|
self._midi_engine.mapping.save()
|
||||||
|
|
||||||
|
def _on_mapping_learned(self, action, target_id, midi_type, channel, note, value):
|
||||||
|
btn = self._learn_btns.get((action, target_id))
|
||||||
|
if btn and btn.isChecked():
|
||||||
|
btn.setChecked(False)
|
||||||
|
btn.setText("OK")
|
||||||
Reference in New Issue
Block a user