v0.5 — cleanup orphan code, add MIDI master volume learn, fix turn-deck crashes
- Remove vestigial lufs_meter_widget.py and debug_audio.py - Delete unused WaveformWidget/WaveformMonitor classes, dead stubs, duplicate code - Fix turn-mode deck crashes (deck3/deck4 missing from engine.decks, uninitialized manual_elapsed/duration) - Replace raw print() with logging in main.py - Add master_volume MIDI learn signal + settings popup entry - Bump version to 0.5
This commit is contained in:
@@ -8,7 +8,7 @@ VIBE CODED AS FUCK. DO WHAT YOU WANT. I DON'T OWN IT YOU DON'T
|
|||||||
| **Playlist Manager** | ✅ | Drag/drop, Add files/folder, Load to deck, Queue, Right-click menu |
|
| **Playlist Manager** | ✅ | Drag/drop, Add files/folder, Load to deck, Queue, Right-click menu |
|
||||||
| **CART Playlist** | ✅ | Separate playlist panel, auto-fills next free cart slot |
|
| **CART Playlist** | ✅ | Separate playlist panel, auto-fills next free cart slot |
|
||||||
| **MIDI Controller** | ✅ | Numark DJ2Go - Play/Pause, Cue, Jog wheels, 4 Cart buttons |
|
| **MIDI Controller** | ✅ | Numark DJ2Go - Play/Pause, Cue, Jog wheels, 4 Cart buttons |
|
||||||
| **Audio Fingerprinting** | ⚠️ | AcoustID integrated, API working but database gaps |
|
|
||||||
| **Play History** | ✅ | Scrollable session history, file tags + MusicBrainz metadata |
|
| **Play History** | ✅ | Scrollable session history, file tags + MusicBrainz metadata |
|
||||||
| **Top Bar** | ✅ | 24hr clock, full date display |
|
| **Top Bar** | ✅ | 24hr clock, full date display |
|
||||||
| **Colour Themes** | ✅ | 4 themes - Green, Amber, Blue, Red - cycle with ◈ button |
|
| **Colour Themes** | ✅ | 4 themes - Green, Amber, Blue, Red - cycle with ◈ button |
|
||||||
@@ -20,7 +20,6 @@ radiopanel/
|
|||||||
├── deck_widget.py # CDJ-style deck UI + transport controls
|
├── deck_widget.py # CDJ-style deck UI + transport controls
|
||||||
├── cart_widget.py # 4-slot CART machine
|
├── cart_widget.py # 4-slot CART machine
|
||||||
├── playlist_widget.py # Playlist + CART playlist panels
|
├── playlist_widget.py # Playlist + CART playlist panels
|
||||||
├── fingerprint_widget.py # AcoustID audio fingerprinting
|
|
||||||
├── midi_engine.py # Numark DJ2Go MIDI mapping
|
├── midi_engine.py # Numark DJ2Go MIDI mapping
|
||||||
├── top_bar_widget.py # Clock, date, theme switcher
|
├── top_bar_widget.py # Clock, date, theme switcher
|
||||||
├── history_widget.py # Session play history
|
├── history_widget.py # Session play history
|
||||||
|
|||||||
+43
-27
@@ -89,6 +89,8 @@ class DeckState:
|
|||||||
self.position = 0
|
self.position = 0
|
||||||
self.playing = False
|
self.playing = False
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
self.manual_elapsed = 0.0
|
||||||
|
self.manual_duration = 0.0
|
||||||
|
|
||||||
def load(self, track: Track):
|
def load(self, track: Track):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -131,15 +133,17 @@ class DeckState:
|
|||||||
self.position = self.track.num_samples - 1
|
self.position = self.track.num_samples - 1
|
||||||
|
|
||||||
def get_elapsed(self) -> str:
|
def get_elapsed(self) -> str:
|
||||||
if not self.track:
|
with self.lock:
|
||||||
return "00:00"
|
if not self.track:
|
||||||
s = self.position / self.track.sr
|
return "00:00"
|
||||||
|
s = self.position / self.track.sr
|
||||||
return f"{int(s//60):02d}:{int(s%60):02d}"
|
return f"{int(s//60):02d}:{int(s%60):02d}"
|
||||||
|
|
||||||
def get_remaining(self) -> str:
|
def get_remaining(self) -> str:
|
||||||
if not self.track:
|
with self.lock:
|
||||||
return "-00:00"
|
if not self.track:
|
||||||
remaining = max(0, self.track.duration - (self.position / self.track.sr))
|
return "-00:00"
|
||||||
|
remaining = max(0, self.track.duration - (self.position / self.track.sr))
|
||||||
return f"-{int(remaining//60):02d}:{int(remaining%60):02d}"
|
return f"-{int(remaining//60):02d}:{int(remaining%60):02d}"
|
||||||
|
|
||||||
|
|
||||||
@@ -228,6 +232,8 @@ class MasterBus:
|
|||||||
self.limiter = Limiter()
|
self.limiter = Limiter()
|
||||||
self.vu_peak = 0.0
|
self.vu_peak = 0.0
|
||||||
self.lufs_current = -70.0
|
self.lufs_current = -70.0
|
||||||
|
self.volume = 1.0
|
||||||
|
self._lufs_accum = 0
|
||||||
|
|
||||||
|
|
||||||
class AudioEngine:
|
class AudioEngine:
|
||||||
@@ -289,6 +295,8 @@ class AudioEngine:
|
|||||||
self.decks = {
|
self.decks = {
|
||||||
'deck1': DeckState(),
|
'deck1': DeckState(),
|
||||||
'deck2': DeckState(),
|
'deck2': DeckState(),
|
||||||
|
'deck3': DeckState(),
|
||||||
|
'deck4': DeckState(),
|
||||||
'cart1': DeckState(),
|
'cart1': DeckState(),
|
||||||
'cart2': DeckState(),
|
'cart2': DeckState(),
|
||||||
'cart3': DeckState(),
|
'cart3': DeckState(),
|
||||||
@@ -301,10 +309,6 @@ class AudioEngine:
|
|||||||
def get_channel(self, index: int) -> MixerChannel:
|
def get_channel(self, index: int) -> MixerChannel:
|
||||||
return self.mixer['channels'][index]
|
return self.mixer['channels'][index]
|
||||||
|
|
||||||
def set_channel_name(self, index: int, name: str):
|
|
||||||
if 0 <= index < NUM_MIXER_CHANNELS:
|
|
||||||
self.mixer['channels'][index].name = name
|
|
||||||
|
|
||||||
def set_channel_volume(self, index: int, volume: float):
|
def set_channel_volume(self, index: int, volume: float):
|
||||||
if 0 <= index < NUM_MIXER_CHANNELS:
|
if 0 <= index < NUM_MIXER_CHANNELS:
|
||||||
self.mixer['channels'][index].volume = max(0.0, min(1.0, volume))
|
self.mixer['channels'][index].volume = max(0.0, min(1.0, volume))
|
||||||
@@ -327,25 +331,30 @@ class AudioEngine:
|
|||||||
buf_L.fill(0.0)
|
buf_L.fill(0.0)
|
||||||
buf_R.fill(0.0)
|
buf_R.fill(0.0)
|
||||||
|
|
||||||
if not deck.playing or deck.track is None:
|
with deck.lock:
|
||||||
|
playing = deck.playing
|
||||||
|
track = deck.track
|
||||||
|
pos = deck.position
|
||||||
|
|
||||||
|
if not playing or track is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
pos = deck.position
|
|
||||||
track = deck.track
|
|
||||||
end = min(pos + frames, track.num_samples)
|
end = min(pos + frames, track.num_samples)
|
||||||
n = end - pos
|
n = end - pos
|
||||||
|
|
||||||
if n <= 0:
|
if n <= 0:
|
||||||
deck.playing = False
|
with deck.lock:
|
||||||
|
deck.playing = False
|
||||||
return
|
return
|
||||||
|
|
||||||
buf_L[:n] = track.audio[0, pos:end]
|
buf_L[:n] = track.audio[0, pos:end]
|
||||||
buf_R[:n] = track.audio[1, pos:end]
|
buf_R[:n] = track.audio[1, pos:end]
|
||||||
deck.position += n
|
|
||||||
|
|
||||||
if deck.position >= track.num_samples:
|
with deck.lock:
|
||||||
deck.playing = False
|
deck.position += n
|
||||||
deck.position = track.num_samples - 1
|
if deck.position >= track.num_samples:
|
||||||
|
deck.playing = False
|
||||||
|
deck.position = track.num_samples - 1
|
||||||
|
|
||||||
def _process(self, frames: int):
|
def _process(self, frames: int):
|
||||||
# ── Digital decks ───────────────────────────────────────────────
|
# ── Digital decks ───────────────────────────────────────────────
|
||||||
@@ -360,21 +369,25 @@ class AudioEngine:
|
|||||||
|
|
||||||
for cart_key in ('cart1', 'cart2', 'cart3', 'cart4'):
|
for cart_key in ('cart1', 'cart2', 'cart3', 'cart4'):
|
||||||
deck = self.decks[cart_key]
|
deck = self.decks[cart_key]
|
||||||
if not deck.playing or deck.track is None:
|
with deck.lock:
|
||||||
|
playing = deck.playing
|
||||||
|
track = deck.track
|
||||||
|
pos = deck.position
|
||||||
|
if not playing or track is None:
|
||||||
continue
|
continue
|
||||||
pos = deck.position
|
|
||||||
track = deck.track
|
|
||||||
end = min(pos + frames, track.num_samples)
|
end = min(pos + frames, track.num_samples)
|
||||||
n = end - pos
|
n = end - pos
|
||||||
if n <= 0:
|
if n <= 0:
|
||||||
deck.playing = False
|
with deck.lock:
|
||||||
|
deck.playing = False
|
||||||
continue
|
continue
|
||||||
buf_L[:n] += track.audio[0, pos:end]
|
buf_L[:n] += track.audio[0, pos:end]
|
||||||
buf_R[:n] += track.audio[1, pos:end]
|
buf_R[:n] += track.audio[1, pos:end]
|
||||||
deck.position += n
|
with deck.lock:
|
||||||
if deck.position >= track.num_samples:
|
deck.position += n
|
||||||
deck.playing = False
|
if deck.position >= track.num_samples:
|
||||||
deck.position = track.num_samples - 1
|
deck.playing = False
|
||||||
|
deck.position = track.num_samples - 1
|
||||||
|
|
||||||
# ── External input monitors (deck3, deck4) ─────────────────────
|
# ── External input monitors (deck3, deck4) ─────────────────────
|
||||||
gate_linear = 10 ** (-46.0 / 20.0)
|
gate_linear = 10 ** (-46.0 / 20.0)
|
||||||
@@ -456,7 +469,10 @@ class AudioEngine:
|
|||||||
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
|
||||||
|
|
||||||
if frames % 512 < frames:
|
master = self.mixer['master']
|
||||||
|
master._lufs_accum += frames
|
||||||
|
if master._lufs_accum >= 512:
|
||||||
|
master._lufs_accum -= 512
|
||||||
self._calculate_lufs(master_audio)
|
self._calculate_lufs(master_audio)
|
||||||
|
|
||||||
def _calculate_lufs(self, audio: np.ndarray):
|
def _calculate_lufs(self, audio: np.ndarray):
|
||||||
|
|||||||
+4
-2
@@ -234,12 +234,14 @@ class CartSlot(QFrame):
|
|||||||
|
|
||||||
def _refresh_display(self):
|
def _refresh_display(self):
|
||||||
deck = self.engine.decks[self.cart_key]
|
deck = self.engine.decks[self.cart_key]
|
||||||
|
track = deck.track
|
||||||
|
playing = deck.playing
|
||||||
|
|
||||||
if not deck.playing and self._playing_icon:
|
if not playing and self._playing_icon:
|
||||||
self._set_ui_stopped()
|
self._set_ui_stopped()
|
||||||
self.status_label.setText("STOPPED")
|
self.status_label.setText("STOPPED")
|
||||||
|
|
||||||
if deck.track:
|
if track:
|
||||||
self.elapsed_label.setText(deck.get_elapsed())
|
self.elapsed_label.setText(deck.get_elapsed())
|
||||||
self.remaining_label.setText(deck.get_remaining())
|
self.remaining_label.setText(deck.get_remaining())
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# debug_audio.py - save and run this
|
|
||||||
import jack
|
|
||||||
import numpy as np
|
|
||||||
import time
|
|
||||||
|
|
||||||
client = jack.Client("DebugTest")
|
|
||||||
client.activate()
|
|
||||||
|
|
||||||
in_L = client.inports.register("test_in_L")
|
|
||||||
in_R = client.inports.register("test_in_R")
|
|
||||||
|
|
||||||
@client.set_process_callback
|
|
||||||
def process(frames):
|
|
||||||
lvl_L = np.max(np.abs(in_L.get_array())) if len(in_L.get_array()) > 0 else 0
|
|
||||||
lvl_R = np.max(np.abs(in_R.get_array())) if len(in_R.get_array()) > 0 else 0
|
|
||||||
if lvl_L > 0.001 or lvl_R > 0.001:
|
|
||||||
print(f"Levels: L={lvl_L:.4f} R={lvl_R:.4f}")
|
|
||||||
|
|
||||||
print("Connect DebugTest:test_in_L/R to your audio source in qpwgraph")
|
|
||||||
print("Press Ctrl+C to stop")
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
time.sleep(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
client.deactivate()
|
|
||||||
+116
-323
@@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
QPushButton, QFrame, QSizePolicy
|
QPushButton, QFrame, QSizePolicy, QInputDialog
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QTimer, Signal, QSize
|
from PySide6.QtCore import Qt, QTimer, Signal, QSize
|
||||||
from PySide6.QtGui import QFont, QColor, QPainter, QPen, QPainterPath
|
from PySide6.QtGui import QFont, QColor
|
||||||
import numpy as np
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from icons import (
|
from icons import (
|
||||||
@@ -15,161 +14,6 @@ from icons import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WaveformWidget(QFrame):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setFixedHeight(60)
|
|
||||||
self.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background-color: #050505;
|
|
||||||
border: 1px solid #1a1a1a;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.waveform_data = None
|
|
||||||
self.position_pct = 0.0
|
|
||||||
self.warning_flash = False
|
|
||||||
|
|
||||||
def set_waveform(self, audio: np.ndarray):
|
|
||||||
if audio is None or audio.shape[1] == 0:
|
|
||||||
self.waveform_data = None
|
|
||||||
self.update()
|
|
||||||
return
|
|
||||||
|
|
||||||
mono = np.mean(audio, axis=0)
|
|
||||||
width = max(500, self.width())
|
|
||||||
samples_per_pixel = max(1, len(mono) // width)
|
|
||||||
num_pixels = len(mono) // samples_per_pixel
|
|
||||||
|
|
||||||
self.waveform_data = np.array([
|
|
||||||
np.sqrt(np.mean(mono[i*samples_per_pixel:(i+1)*samples_per_pixel]**2))
|
|
||||||
for i in range(num_pixels)
|
|
||||||
], dtype=np.float32)
|
|
||||||
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def set_position(self, position_pct: float, warning: bool = False):
|
|
||||||
self.position_pct = max(0.0, min(1.0, position_pct))
|
|
||||||
self.warning_flash = warning
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
super().paintEvent(event)
|
|
||||||
|
|
||||||
if self.waveform_data is None or len(self.waveform_data) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
|
||||||
|
|
||||||
rect = self.rect().adjusted(2, 2, -2, -2)
|
|
||||||
width = rect.width()
|
|
||||||
height = rect.height()
|
|
||||||
|
|
||||||
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)
|
|
||||||
x_scale = width / len(self.waveform_data)
|
|
||||||
|
|
||||||
for i, rms in enumerate(self.waveform_data):
|
|
||||||
x = rect.x() + int(i * x_scale)
|
|
||||||
bar_h = int(scaled[i])
|
|
||||||
y_top = rect.y() + (height - bar_h) // 2
|
|
||||||
|
|
||||||
pct = i / len(self.waveform_data)
|
|
||||||
if pct < self.position_pct:
|
|
||||||
color = QColor("#004400")
|
|
||||||
elif pct > 0.85:
|
|
||||||
color = QColor("#ff0000") if self.warning_flash else QColor("#aa0000")
|
|
||||||
elif pct > 0.70:
|
|
||||||
color = QColor("#ffaa00")
|
|
||||||
else:
|
|
||||||
color = QColor("#00ff41")
|
|
||||||
|
|
||||||
pen = QPen(color, 1)
|
|
||||||
painter.setPen(pen)
|
|
||||||
painter.drawLine(x, y_top, x, y_top + bar_h)
|
|
||||||
|
|
||||||
playhead_x = rect.x() + int(self.position_pct * width)
|
|
||||||
pen = QPen(QColor("#ffffff"), 2)
|
|
||||||
painter.setPen(pen)
|
|
||||||
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)
|
||||||
@@ -186,11 +30,17 @@ class LCDDisplay(QFrame):
|
|||||||
layout.setContentsMargins(10, 8, 10, 8)
|
layout.setContentsMargins(10, 8, 10, 8)
|
||||||
layout.setSpacing(4)
|
layout.setSpacing(4)
|
||||||
|
|
||||||
self.track_label = QLabel("NO TRACK LOADED")
|
self.track_btn = QPushButton("NO TRACK LOADED")
|
||||||
self.track_label.setFont(QFont("Courier New", 13, QFont.Bold))
|
self.track_btn.setFont(QFont("Courier New", 13, QFont.Bold))
|
||||||
self.track_label.setStyleSheet("color: #00ff41; background: transparent;")
|
self.track_btn.setStyleSheet("""
|
||||||
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
QPushButton {
|
||||||
self.track_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
color: #00ff41; background: transparent; border: none;
|
||||||
|
text-align: left; padding: 0px;
|
||||||
|
}
|
||||||
|
QPushButton:hover { color: #ffffff; }
|
||||||
|
""")
|
||||||
|
self.track_btn.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.track_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
|
|
||||||
time_row = QHBoxLayout()
|
time_row = QHBoxLayout()
|
||||||
time_row.setSpacing(4)
|
time_row.setSpacing(4)
|
||||||
@@ -224,14 +74,17 @@ class LCDDisplay(QFrame):
|
|||||||
status_row.addStretch()
|
status_row.addStretch()
|
||||||
status_row.addWidget(self.queue_label)
|
status_row.addWidget(self.queue_label)
|
||||||
|
|
||||||
layout.addWidget(self.track_label)
|
layout.addWidget(self.track_btn)
|
||||||
layout.addLayout(time_row)
|
layout.addLayout(time_row)
|
||||||
layout.addLayout(status_row)
|
layout.addLayout(status_row)
|
||||||
|
|
||||||
def update_track(self, name: str):
|
def update_track(self, name: str):
|
||||||
if len(name) > 40:
|
if len(name) > 40:
|
||||||
name = name[:37] + "..."
|
name = name[:37] + "..."
|
||||||
self.track_label.setText(name)
|
self.track_btn.setText(name)
|
||||||
|
|
||||||
|
def track_name(self) -> str:
|
||||||
|
return self.track_btn.text()
|
||||||
|
|
||||||
def update_time(self, elapsed: str, remaining: str):
|
def update_time(self, elapsed: str, remaining: str):
|
||||||
self.elapsed_label.setText(elapsed)
|
self.elapsed_label.setText(elapsed)
|
||||||
@@ -287,7 +140,7 @@ class DeckWidget(QWidget):
|
|||||||
track_started = Signal(str)
|
track_started = Signal(str)
|
||||||
|
|
||||||
def __init__(self, deck_key: str, label: str, accent_color: str,
|
def __init__(self, deck_key: str, label: str, accent_color: str,
|
||||||
engine, parent=None):
|
engine, parent=None, mode='deck'):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.deck_key = deck_key
|
self.deck_key = deck_key
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
@@ -295,9 +148,9 @@ 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.mode = mode
|
||||||
|
|
||||||
deck_num = label.split()[-1]
|
self.deck_label = label
|
||||||
self.deck_label = f"DECK_{int(deck_num):03d}"
|
|
||||||
|
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
@@ -339,9 +192,6 @@ class DeckWidget(QWidget):
|
|||||||
self.lcd = LCDDisplay()
|
self.lcd = LCDDisplay()
|
||||||
root.addWidget(self.lcd)
|
root.addWidget(self.lcd)
|
||||||
|
|
||||||
self.waveform = WaveformWidget()
|
|
||||||
root.addWidget(self.waveform)
|
|
||||||
|
|
||||||
transport = QHBoxLayout()
|
transport = QHBoxLayout()
|
||||||
transport.setSpacing(6)
|
transport.setSpacing(6)
|
||||||
|
|
||||||
@@ -362,6 +212,12 @@ class DeckWidget(QWidget):
|
|||||||
|
|
||||||
root.addLayout(transport)
|
root.addLayout(transport)
|
||||||
|
|
||||||
|
if self.mode == 'turn':
|
||||||
|
self.btn_to_start.setVisible(False)
|
||||||
|
self.btn_back_15.setVisible(False)
|
||||||
|
self.btn_fwd_15.setVisible(False)
|
||||||
|
self.btn_to_end.setVisible(False)
|
||||||
|
|
||||||
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']
|
||||||
@@ -381,13 +237,6 @@ class DeckWidget(QWidget):
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
self.waveform.setStyleSheet(f"""
|
|
||||||
QFrame {{
|
|
||||||
background-color: {theme['bg']};
|
|
||||||
border: 1px solid {border_color};
|
|
||||||
border-radius: 3px;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
|
|
||||||
def _toggle_flash(self):
|
def _toggle_flash(self):
|
||||||
self.flash_state = not self.flash_state
|
self.flash_state = not self.flash_state
|
||||||
@@ -395,21 +244,39 @@ class DeckWidget(QWidget):
|
|||||||
def _connect_signals(self):
|
def _connect_signals(self):
|
||||||
self.btn_play.clicked.connect(self._toggle_play)
|
self.btn_play.clicked.connect(self._toggle_play)
|
||||||
self.btn_stop.clicked.connect(self._stop_eject)
|
self.btn_stop.clicked.connect(self._stop_eject)
|
||||||
self.btn_to_start.clicked.connect(self._go_to_start)
|
self.lcd.track_btn.clicked.connect(self._edit_track_name)
|
||||||
self.btn_to_end.clicked.connect(self._go_to_end)
|
if self.mode != 'turn':
|
||||||
self.btn_back_15.clicked.connect(lambda: self._scrub(-15))
|
self.btn_to_start.clicked.connect(self._go_to_start)
|
||||||
self.btn_fwd_15.clicked.connect(lambda: self._scrub(15))
|
self.btn_to_end.clicked.connect(self._go_to_end)
|
||||||
|
self.btn_back_15.clicked.connect(lambda: self._scrub(-15))
|
||||||
|
self.btn_fwd_15.clicked.connect(lambda: self._scrub(15))
|
||||||
|
|
||||||
|
def _edit_track_name(self):
|
||||||
|
current = self.lcd.track_btn.text()
|
||||||
|
if current == "NO TRACK LOADED":
|
||||||
|
current = ""
|
||||||
|
name, ok = QInputDialog.getText(self, "Track Name", "Track name:", text=current)
|
||||||
|
if ok:
|
||||||
|
if not name.strip():
|
||||||
|
name = "NO TRACK LOADED"
|
||||||
|
self.lcd.update_track(name.strip())
|
||||||
|
if self.mode == 'turn':
|
||||||
|
deck = self.engine.decks[self.deck_key]
|
||||||
|
if self.mode == 'turn' and name.strip() != "NO TRACK LOADED":
|
||||||
|
dur, ok2 = QInputDialog.getText(self, "Duration", "Duration (MM:SS):", text="00:00")
|
||||||
|
if ok2 and ':' in dur:
|
||||||
|
parts = dur.split(':')
|
||||||
|
try:
|
||||||
|
total_sec = int(parts[0]) * 60 + int(parts[1])
|
||||||
|
deck = self.engine.decks[self.deck_key]
|
||||||
|
deck.manual_duration = float(total_sec)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
def set_track_name(self, name: str):
|
def set_track_name(self, name: str):
|
||||||
self.lcd.update_track(name)
|
self.lcd.update_track(name)
|
||||||
self.lcd.update_status("LOADED")
|
self.lcd.update_status("LOADED")
|
||||||
self.lcd.update_time("00:00", "-00:00")
|
self.lcd.update_time("00:00", "-00:00")
|
||||||
QTimer.singleShot(200, self._update_waveform)
|
|
||||||
|
|
||||||
def _update_waveform(self):
|
|
||||||
deck = self.engine.decks[self.deck_key]
|
|
||||||
if deck.track:
|
|
||||||
self.waveform.set_waveform(deck.track.audio)
|
|
||||||
|
|
||||||
def set_queue_name(self, name: str):
|
def set_queue_name(self, name: str):
|
||||||
self.lcd.update_queue(name)
|
self.lcd.update_queue(name)
|
||||||
@@ -418,6 +285,8 @@ class DeckWidget(QWidget):
|
|||||||
self._toggle_play()
|
self._toggle_play()
|
||||||
|
|
||||||
def cue(self):
|
def cue(self):
|
||||||
|
if self.mode == 'turn':
|
||||||
|
return
|
||||||
deck = self.engine.decks[self.deck_key]
|
deck = self.engine.decks[self.deck_key]
|
||||||
if deck.track is None:
|
if deck.track is None:
|
||||||
return
|
return
|
||||||
@@ -442,10 +311,13 @@ class DeckWidget(QWidget):
|
|||||||
|
|
||||||
def _toggle_play(self):
|
def _toggle_play(self):
|
||||||
deck = self.engine.decks[self.deck_key]
|
deck = self.engine.decks[self.deck_key]
|
||||||
if deck.track is None:
|
if deck.track is None and self.mode != 'turn':
|
||||||
return
|
return
|
||||||
if deck.playing:
|
if deck.playing:
|
||||||
deck.pause()
|
if self.mode == 'turn':
|
||||||
|
deck.playing = False
|
||||||
|
else:
|
||||||
|
deck.pause()
|
||||||
self.btn_play.setIcon(icon_play("#00ff41"))
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
||||||
self._playing_icon = False
|
self._playing_icon = False
|
||||||
self.btn_play.setStyleSheet("""
|
self.btn_play.setStyleSheet("""
|
||||||
@@ -456,10 +328,13 @@ class DeckWidget(QWidget):
|
|||||||
}
|
}
|
||||||
QPushButton:hover { background-color: #2a7b4a; }
|
QPushButton:hover { background-color: #2a7b4a; }
|
||||||
""")
|
""")
|
||||||
self.lcd.update_status("PAUSED")
|
self.lcd.update_status("PAUSED" if self.mode != 'turn' else "STOPPED")
|
||||||
self.lcd.set_playing(False)
|
self.lcd.set_playing(False)
|
||||||
else:
|
else:
|
||||||
deck.play()
|
if self.mode == 'turn':
|
||||||
|
deck.playing = True
|
||||||
|
else:
|
||||||
|
deck.play()
|
||||||
self.btn_play.setIcon(icon_pause("#ffffff"))
|
self.btn_play.setIcon(icon_pause("#ffffff"))
|
||||||
self._playing_icon = True
|
self._playing_icon = True
|
||||||
self.btn_play.setStyleSheet("""
|
self.btn_play.setStyleSheet("""
|
||||||
@@ -472,12 +347,21 @@ class DeckWidget(QWidget):
|
|||||||
""")
|
""")
|
||||||
self.lcd.update_status("PLAYING")
|
self.lcd.update_status("PLAYING")
|
||||||
self.lcd.set_playing(True)
|
self.lcd.set_playing(True)
|
||||||
if deck.track:
|
if self.mode == 'turn':
|
||||||
|
deck.manual_elapsed = 0.0
|
||||||
|
name = self.lcd.track_btn.text()
|
||||||
|
if name and name != "NO TRACK LOADED":
|
||||||
|
self.track_started.emit(f"[TURN] {name}")
|
||||||
|
elif deck.track:
|
||||||
self.track_started.emit(deck.track.filepath)
|
self.track_started.emit(deck.track.filepath)
|
||||||
|
|
||||||
def _stop_eject(self):
|
def _stop_eject(self):
|
||||||
deck = self.engine.decks[self.deck_key]
|
deck = self.engine.decks[self.deck_key]
|
||||||
deck.stop()
|
if self.mode == 'turn':
|
||||||
|
deck.playing = False
|
||||||
|
deck.manual_elapsed = 0.0
|
||||||
|
else:
|
||||||
|
deck.stop()
|
||||||
self.btn_play.setIcon(icon_play("#00ff41"))
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
||||||
self._playing_icon = False
|
self._playing_icon = False
|
||||||
self.btn_play.setStyleSheet("""
|
self.btn_play.setStyleSheet("""
|
||||||
@@ -492,7 +376,6 @@ class DeckWidget(QWidget):
|
|||||||
self.lcd.update_time("00:00", "-00:00")
|
self.lcd.update_time("00:00", "-00:00")
|
||||||
self.lcd.update_status("STOPPED")
|
self.lcd.update_status("STOPPED")
|
||||||
self.lcd.set_playing(False)
|
self.lcd.set_playing(False)
|
||||||
self.waveform.set_waveform(None)
|
|
||||||
|
|
||||||
def _go_to_start(self):
|
def _go_to_start(self):
|
||||||
self.engine.decks[self.deck_key].go_to_start()
|
self.engine.decks[self.deck_key].go_to_start()
|
||||||
@@ -506,17 +389,42 @@ 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]
|
||||||
|
|
||||||
if not deck.playing and deck.track is not None:
|
if self.mode == 'turn':
|
||||||
if deck.position >= deck.track.num_samples - 1:
|
if deck.playing:
|
||||||
if deck.queued:
|
deck.manual_elapsed += 0.1
|
||||||
deck.load(deck.queued)
|
if deck.manual_duration > 0 and deck.manual_elapsed >= deck.manual_duration:
|
||||||
self.lcd.update_track(os.path.splitext(os.path.basename(deck.queued.filepath))[0])
|
deck.playing = False
|
||||||
QTimer.singleShot(200, self._update_waveform)
|
self.btn_play.setIcon(icon_play("#00ff41"))
|
||||||
|
self._playing_icon = False
|
||||||
|
self.lcd.update_status("STOPPED")
|
||||||
|
self.lcd.set_playing(False)
|
||||||
|
|
||||||
|
elapsed_s = int(deck.manual_elapsed)
|
||||||
|
elapsed = f"{elapsed_s // 60:02d}:{elapsed_s % 60:02d}"
|
||||||
|
if deck.manual_duration > 0:
|
||||||
|
remaining = max(0, deck.manual_duration - deck.manual_elapsed)
|
||||||
|
remaining_s = int(remaining)
|
||||||
|
remaining_str = f"-{remaining_s // 60:02d}:{remaining_s % 60:02d}"
|
||||||
|
else:
|
||||||
|
remaining_str = "-00:00"
|
||||||
|
self.lcd.update_time(elapsed, remaining_str)
|
||||||
|
return
|
||||||
|
|
||||||
|
track = deck.track
|
||||||
|
playing = deck.playing
|
||||||
|
position = deck.position
|
||||||
|
queued = deck.queued
|
||||||
|
|
||||||
|
if not playing and track is not None:
|
||||||
|
if position >= track.num_samples - 1:
|
||||||
|
if queued:
|
||||||
|
deck.load(queued)
|
||||||
|
self.lcd.update_track(os.path.splitext(os.path.basename(queued.filepath))[0])
|
||||||
deck.queued = None
|
deck.queued = None
|
||||||
self.lcd.update_queue("")
|
self.lcd.update_queue("")
|
||||||
self.lcd.update_status("LOADED")
|
self.lcd.update_status("LOADED")
|
||||||
|
|
||||||
if not deck.playing and self._playing_icon:
|
if not 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
|
||||||
self.btn_play.setStyleSheet("""
|
self.btn_play.setStyleSheet("""
|
||||||
@@ -530,13 +438,13 @@ class DeckWidget(QWidget):
|
|||||||
self.lcd.update_status("STOPPED")
|
self.lcd.update_status("STOPPED")
|
||||||
self.lcd.set_playing(False)
|
self.lcd.set_playing(False)
|
||||||
|
|
||||||
if deck.track:
|
if track:
|
||||||
self.lcd.update_time(deck.get_elapsed(), deck.get_remaining())
|
self.lcd.update_time(deck.get_elapsed(), deck.get_remaining())
|
||||||
position_pct = deck.position / deck.track.num_samples
|
position_pct = position / track.num_samples
|
||||||
remaining_sec = deck.track.duration - (deck.position / deck.track.sr)
|
remaining_sec = track.duration - (position / track.sr)
|
||||||
in_warning = (remaining_sec < 20) or (position_pct > 0.85)
|
in_warning = (remaining_sec < 20) or (position_pct > 0.85)
|
||||||
|
|
||||||
if deck.playing and in_warning:
|
if 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
|
||||||
self.header.setStyleSheet(f"""
|
self.header.setStyleSheet(f"""
|
||||||
color: {flash_color};
|
color: {flash_color};
|
||||||
@@ -554,120 +462,5 @@ class DeckWidget(QWidget):
|
|||||||
""")
|
""")
|
||||||
self.lcd.flash_warning(False)
|
self.lcd.flash_warning(False)
|
||||||
|
|
||||||
self.waveform.set_position(position_pct, in_warning and self.flash_state)
|
if queued:
|
||||||
|
self.lcd.update_queue(os.path.basename(queued.filepath))
|
||||||
if deck.queued:
|
|
||||||
self.lcd.update_queue(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;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# history_widget.py
|
# history_widget.py
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
|||||||
@@ -312,39 +312,6 @@ def icon_cog(color: str = "#aaaaaa") -> 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_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,139 +0,0 @@
|
|||||||
# lufs_meter_widget.py
|
|
||||||
|
|
||||||
from PySide6.QtWidgets import QFrame, QVBoxLayout, QLabel, QWidget
|
|
||||||
from PySide6.QtCore import Qt, QTimer
|
|
||||||
from PySide6.QtGui import QFont, QPainter, QColor, QLinearGradient, QPen
|
|
||||||
|
|
||||||
|
|
||||||
class LUFSMeterWidget(QFrame):
|
|
||||||
"""Vertical LUFS meter with -14 LUFS target for streaming/broadcast."""
|
|
||||||
|
|
||||||
def __init__(self, engine, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.engine = engine
|
|
||||||
|
|
||||||
self.setFixedWidth(80)
|
|
||||||
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
|
||||||
self.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background-color: #0d0d0d;
|
|
||||||
border: 2px solid #444444;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
self._build_ui()
|
|
||||||
|
|
||||||
self.timer = QTimer(self)
|
|
||||||
self.timer.setInterval(100)
|
|
||||||
self.timer.timeout.connect(self._update_meter)
|
|
||||||
self.timer.start()
|
|
||||||
|
|
||||||
def _build_ui(self):
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
layout.setContentsMargins(6, 8, 6, 8)
|
|
||||||
|
|
||||||
header = QLabel("LUFS")
|
|
||||||
header.setFont(QFont("Arial", 10, QFont.Bold))
|
|
||||||
header.setStyleSheet("color: #888888; background: transparent; border: none;")
|
|
||||||
header.setAlignment(Qt.AlignCenter)
|
|
||||||
layout.addWidget(header)
|
|
||||||
|
|
||||||
self.meter_canvas = MeterCanvas(self.engine)
|
|
||||||
layout.addWidget(self.meter_canvas, stretch=1)
|
|
||||||
|
|
||||||
self.readout = QLabel("-70.0")
|
|
||||||
self.readout.setFont(QFont("Courier New", 12, QFont.Bold))
|
|
||||||
self.readout.setStyleSheet("color: #00ff41; background: transparent; border: none;")
|
|
||||||
self.readout.setAlignment(Qt.AlignCenter)
|
|
||||||
layout.addWidget(self.readout)
|
|
||||||
|
|
||||||
def _update_meter(self):
|
|
||||||
lufs = self.engine.lufs_current
|
|
||||||
self.readout.setText(f"{lufs:.1f}")
|
|
||||||
self.meter_canvas.update()
|
|
||||||
|
|
||||||
|
|
||||||
class MeterCanvas(QWidget):
|
|
||||||
"""Custom painted LUFS bar with -14 LUFS target."""
|
|
||||||
|
|
||||||
def __init__(self, engine, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.engine = engine
|
|
||||||
self.setStyleSheet("""
|
|
||||||
QWidget {
|
|
||||||
background-color: #000000;
|
|
||||||
border: 1px solid #333333;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.setMinimumHeight(200)
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
|
||||||
|
|
||||||
rect = self.rect().adjusted(3, 3, -3, -3)
|
|
||||||
lufs = self.engine.lufs_current
|
|
||||||
|
|
||||||
# Scale: -50 LUFS (bottom) to 0 (top) - makes -14 more readable
|
|
||||||
lufs_min = -50
|
|
||||||
lufs_max = 0
|
|
||||||
lufs_range = lufs_max - lufs_min
|
|
||||||
|
|
||||||
lufs = max(lufs_min, min(lufs_max, lufs))
|
|
||||||
fill_pct = (lufs - lufs_min) / lufs_range
|
|
||||||
fill_h = int(rect.height() * fill_pct)
|
|
||||||
|
|
||||||
# Color zones optimized for -14 LUFS target
|
|
||||||
gradient = QLinearGradient(0, rect.bottom(), 0, rect.top())
|
|
||||||
|
|
||||||
if lufs < -30:
|
|
||||||
# Very quiet - dark green
|
|
||||||
gradient.setColorAt(0, QColor("#002200"))
|
|
||||||
gradient.setColorAt(1, QColor("#004400"))
|
|
||||||
elif lufs < -18:
|
|
||||||
# Quiet - green
|
|
||||||
gradient.setColorAt(0, QColor("#00aa00"))
|
|
||||||
gradient.setColorAt(1, QColor("#00dd00"))
|
|
||||||
elif lufs < -12:
|
|
||||||
# Target zone -18 to -12 LUFS (±4 LU around -14) - bright green
|
|
||||||
gradient.setColorAt(0, QColor("#00ff00"))
|
|
||||||
gradient.setColorAt(1, QColor("#00ff00"))
|
|
||||||
elif lufs < -6:
|
|
||||||
# Getting hot - yellow
|
|
||||||
gradient.setColorAt(0, QColor("#ffcc00"))
|
|
||||||
gradient.setColorAt(1, QColor("#ffaa00"))
|
|
||||||
else:
|
|
||||||
# Too hot - red
|
|
||||||
gradient.setColorAt(0, QColor("#ff0000"))
|
|
||||||
gradient.setColorAt(1, QColor("#ff0000"))
|
|
||||||
|
|
||||||
painter.fillRect(
|
|
||||||
rect.x(),
|
|
||||||
rect.bottom() - fill_h,
|
|
||||||
rect.width(),
|
|
||||||
fill_h,
|
|
||||||
gradient
|
|
||||||
)
|
|
||||||
|
|
||||||
# Draw reference lines
|
|
||||||
painter.setPen(QPen(QColor("#ffffff"), 1))
|
|
||||||
|
|
||||||
# Target line at -14 LUFS (main target)
|
|
||||||
target_pct = ((-14 - lufs_min) / lufs_range)
|
|
||||||
target_y = rect.bottom() - int(rect.height() * target_pct)
|
|
||||||
painter.drawLine(rect.x(), target_y, rect.right(), target_y)
|
|
||||||
|
|
||||||
# Upper boundary at -12 LUFS (dotted)
|
|
||||||
painter.setPen(QPen(QColor("#888888"), 1, Qt.DotLine))
|
|
||||||
upper_pct = ((-12 - lufs_min) / lufs_range)
|
|
||||||
upper_y = rect.bottom() - int(rect.height() * upper_pct)
|
|
||||||
painter.drawLine(rect.x(), upper_y, rect.right(), upper_y)
|
|
||||||
|
|
||||||
# Lower boundary at -16 LUFS (dotted)
|
|
||||||
lower_pct = ((-16 - lufs_min) / lufs_range)
|
|
||||||
lower_y = rect.bottom() - int(rect.height() * lower_pct)
|
|
||||||
painter.drawLine(rect.x(), lower_y, rect.right(), lower_y)
|
|
||||||
|
|
||||||
painter.end()
|
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
# main.py
|
# main.py
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
__version__ = "0.5"
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QFileDialog,
|
QApplication, QMainWindow, QWidget, QFileDialog,
|
||||||
QVBoxLayout, QHBoxLayout, QSplitter
|
QVBoxLayout, QHBoxLayout, QSplitter
|
||||||
@@ -11,7 +15,7 @@ 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, ExternalDeckWidget
|
from deck_widget import DeckWidget
|
||||||
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
|
||||||
@@ -19,6 +23,8 @@ from top_bar_widget import TopBarWidget, THEMES
|
|||||||
from history_widget import HistoryWidget
|
from history_widget import HistoryWidget
|
||||||
from mixer_widget import MixerWidget
|
from mixer_widget import MixerWidget
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RadioPanelWindow(QMainWindow):
|
class RadioPanelWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -26,7 +32,7 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
|
|
||||||
self.engine = AudioEngine()
|
self.engine = AudioEngine()
|
||||||
|
|
||||||
self.setWindowTitle("Getting To It")
|
self.setWindowTitle(f"Getting To It v{__version__}")
|
||||||
self.setMinimumSize(1400, 860)
|
self.setMinimumSize(1400, 860)
|
||||||
self.resize(1920, 1080)
|
self.resize(1920, 1080)
|
||||||
self._apply_global_style(THEMES[0])
|
self._apply_global_style(THEMES[0])
|
||||||
@@ -48,10 +54,10 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
|
|
||||||
# ── Build widgets ─────────────────────────────────────────────────
|
# ── Build widgets ─────────────────────────────────────────────────
|
||||||
|
|
||||||
self.deck1 = DeckWidget("deck1", "DECK 1", "#00b894", self.engine)
|
self.deck1 = DeckWidget("deck1", "DECK_001", "#00b894", self.engine, mode='deck')
|
||||||
self.deck2 = DeckWidget("deck2", "DECK 2", "#6c5ce7", self.engine)
|
self.deck2 = DeckWidget("deck2", "DECK_002", "#6c5ce7", self.engine, mode='deck')
|
||||||
self.deck3 = ExternalDeckWidget("deck3", "DECK 3", "#fdcb6e", self.engine)
|
self.deck3 = DeckWidget("deck3", "TURN_001", "#fdcb6e", self.engine, mode='turn')
|
||||||
self.deck4 = ExternalDeckWidget("deck4", "DECK 4", "#e17055", self.engine)
|
self.deck4 = DeckWidget("deck4", "TURN_002", "#e17055", self.engine, mode='turn')
|
||||||
|
|
||||||
self.cart_bank = CartBankWidget(self.engine)
|
self.cart_bank = CartBankWidget(self.engine)
|
||||||
self.cart_bank.setFixedHeight(180)
|
self.cart_bank.setFixedHeight(180)
|
||||||
@@ -117,6 +123,8 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
# ── 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)
|
||||||
|
self.deck3.track_started.connect(self.history_widget.add_track)
|
||||||
|
self.deck4.track_started.connect(self.history_widget.add_track)
|
||||||
|
|
||||||
# ── MIDI ──────────────────────────────────────────────────────────
|
# ── MIDI ──────────────────────────────────────────────────────────
|
||||||
self._setup_midi()
|
self._setup_midi()
|
||||||
@@ -233,6 +241,7 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
self.midi.mixer_solo.connect(self._on_midi_mixer_solo)
|
self.midi.mixer_solo.connect(self._on_midi_mixer_solo)
|
||||||
self.midi.mixer_pfl.connect(self._on_midi_mixer_pfl)
|
self.midi.mixer_pfl.connect(self._on_midi_mixer_pfl)
|
||||||
self.midi.master_control.connect(self._on_midi_master_control)
|
self.midi.master_control.connect(self._on_midi_master_control)
|
||||||
|
self.midi.master_volume.connect(self._on_midi_master_volume)
|
||||||
self.midi.controller_connected.connect(self._on_midi_status)
|
self.midi.controller_connected.connect(self._on_midi_status)
|
||||||
|
|
||||||
self.midi.start()
|
self.midi.start()
|
||||||
@@ -240,7 +249,7 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
|
|
||||||
ports = self.midi.list_ports()
|
ports = self.midi.list_ports()
|
||||||
if ports:
|
if ports:
|
||||||
print(f"[MIDI] Available ports: {ports}")
|
logger.info("Available ports: %s", ports)
|
||||||
|
|
||||||
def _on_midi_play_pause(self, deck: str):
|
def _on_midi_play_pause(self, deck: str):
|
||||||
if deck == 'deck1':
|
if deck == 'deck1':
|
||||||
@@ -298,11 +307,14 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
v = round(max(lo, min(hi, v)), 1)
|
v = round(max(lo, min(hi, v)), 1)
|
||||||
setattr(master.compressor if knob != "ceiling_db" else master.limiter, knob, v)
|
setattr(master.compressor if knob != "ceiling_db" else master.limiter, knob, v)
|
||||||
|
|
||||||
|
def _on_midi_master_volume(self, val: float):
|
||||||
|
self.engine.mixer['master'].volume = max(0.0, min(1.0, val))
|
||||||
|
|
||||||
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}")
|
logger.info("Connected: %s", name)
|
||||||
else:
|
else:
|
||||||
print(f"[MIDI] Not connected: {name}")
|
logger.info("Not connected: %s", name)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
self.midi.stop()
|
self.midi.stop()
|
||||||
|
|||||||
+7
-2
@@ -19,7 +19,6 @@ NOTE_ON = 0x90
|
|||||||
NOTE_OFF = 0x80
|
NOTE_OFF = 0x80
|
||||||
CC = 0xB0
|
CC = 0xB0
|
||||||
PITCH_BEND = 0xE0
|
PITCH_BEND = 0xE0
|
||||||
PITCH_BEND = 0xE0
|
|
||||||
|
|
||||||
CONFIG_DIR = os.path.expanduser("~/.gti_radiostudio")
|
CONFIG_DIR = os.path.expanduser("~/.gti_radiostudio")
|
||||||
CONFIG_FILE = os.path.join(CONFIG_DIR, "midi_map.json")
|
CONFIG_FILE = os.path.join(CONFIG_DIR, "midi_map.json")
|
||||||
@@ -96,6 +95,7 @@ class MidiEngine(QObject):
|
|||||||
mixer_solo = Signal(int, bool)
|
mixer_solo = Signal(int, bool)
|
||||||
mixer_pfl = Signal(int, bool)
|
mixer_pfl = Signal(int, bool)
|
||||||
master_control = Signal(str, float)
|
master_control = Signal(str, float)
|
||||||
|
master_volume = Signal(float)
|
||||||
controller_connected = Signal(bool, str)
|
controller_connected = Signal(bool, str)
|
||||||
mapping_learned = Signal(str, object, int, int, int, bool)
|
mapping_learned = Signal(str, object, int, int, int, bool)
|
||||||
raw_midi_event = Signal(str, int, int, int)
|
raw_midi_event = Signal(str, int, int, int)
|
||||||
@@ -115,7 +115,6 @@ class MidiEngine(QObject):
|
|||||||
self._last_cc_values = {}
|
self._last_cc_values = {}
|
||||||
self._channel_volumes = {}
|
self._channel_volumes = {}
|
||||||
self._master_knob_values = {}
|
self._master_knob_values = {}
|
||||||
self._master_knob_values = {}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def learn_mode(self) -> bool:
|
def learn_mode(self) -> bool:
|
||||||
@@ -303,6 +302,12 @@ class MidiEngine(QObject):
|
|||||||
scaled = (127 - value if inv else value) / 127.0
|
scaled = (127 - value if inv else value) / 127.0
|
||||||
self._master_knob_values[knob] = scaled
|
self._master_knob_values[knob] = scaled
|
||||||
self.master_control.emit(knob, scaled)
|
self.master_control.emit(knob, scaled)
|
||||||
|
elif action == 'master_volume':
|
||||||
|
if midi_type == PITCH_BEND:
|
||||||
|
scaled = (16383 - value if inv else value) / 16383.0
|
||||||
|
else:
|
||||||
|
scaled = (127 - value if inv else value) / 127.0
|
||||||
|
self.master_volume.emit(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)
|
||||||
|
|||||||
+161
-183
@@ -93,8 +93,8 @@ 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.setMinimumWidth(90)
|
self.setFixedWidth(90)
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
ChannelStrip {
|
ChannelStrip {
|
||||||
background-color: #181a1e;
|
background-color: #181a1e;
|
||||||
@@ -269,32 +269,25 @@ class ChannelStrip(QFrame):
|
|||||||
def _refresh(self):
|
def _refresh(self):
|
||||||
self.vu_meter.set_value(self._channel.vu_peak)
|
self.vu_meter.set_value(self._channel.vu_peak)
|
||||||
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
||||||
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
|
||||||
|
|
||||||
def apply_theme(self, theme: dict):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MasterSection(QFrame):
|
class MasterPreSection(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._master = engine.mixer['master']
|
self._master = engine.mixer['master']
|
||||||
|
|
||||||
self._knobs = {}
|
self.setFixedWidth(80)
|
||||||
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||||
self.setMinimumWidth(180)
|
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
MasterSection {
|
MasterPreSection {
|
||||||
background-color: #181a1e;
|
background-color: #101216;
|
||||||
border: 1px solid #282a30;
|
border: 1px solid #1e2026;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._connect_signals()
|
|
||||||
|
|
||||||
self.timer = QTimer(self)
|
self.timer = QTimer(self)
|
||||||
self.timer.setInterval(30)
|
self.timer.setInterval(30)
|
||||||
@@ -303,158 +296,158 @@ class MasterSection(QFrame):
|
|||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
root.setContentsMargins(8, 6, 8, 6)
|
root.setContentsMargins(4, 6, 4, 6)
|
||||||
root.setSpacing(4)
|
root.setSpacing(4)
|
||||||
|
|
||||||
header = QLabel("MASTER")
|
header = QLabel("MASTER")
|
||||||
header.setFont(QFont("Segoe UI", 10, QFont.Bold))
|
header.setFont(QFont("Segoe UI", 8, QFont.Bold))
|
||||||
header.setAlignment(Qt.AlignCenter)
|
header.setAlignment(Qt.AlignCenter)
|
||||||
header.setStyleSheet("color: #00cc88; background: transparent; border: none;")
|
header.setStyleSheet("color: #888; background: transparent; border: none;")
|
||||||
root.addWidget(header)
|
root.addWidget(header)
|
||||||
|
|
||||||
|
meter_row = QHBoxLayout()
|
||||||
|
meter_row.setSpacing(2)
|
||||||
|
|
||||||
self.vu_meter = VUMeter()
|
self.vu_meter = VUMeter()
|
||||||
root.addWidget(self.vu_meter)
|
meter_row.addWidget(self.vu_meter, stretch=1)
|
||||||
|
|
||||||
self.lufs_label = QLabel("-70.0 LUFS")
|
self.volume_slider = QSlider(Qt.Vertical)
|
||||||
self.lufs_label.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
self.volume_slider.setRange(0, 100)
|
||||||
self.lufs_label.setAlignment(Qt.AlignCenter)
|
self.volume_slider.setValue(int(math.sqrt(self._master.volume) * 100))
|
||||||
self.lufs_label.setStyleSheet("color: #00cc88; background: transparent; border: none;")
|
self.volume_slider.setStyleSheet("""
|
||||||
root.addWidget(self.lufs_label)
|
QSlider::groove:vertical { background: #22242a; width: 4px; border-radius: 2px; }
|
||||||
|
QSlider::handle:vertical { background: #8890a0; height: 12px; width: 14px; margin: -4px -5px; border-radius: 2px; }
|
||||||
pfl_indicator = QLabel("PFL: —")
|
QSlider::handle:vertical:hover { background: #b0b8c8; }
|
||||||
self.pfl_indicator = pfl_indicator
|
QSlider::add-page:vertical {
|
||||||
pfl_indicator.setFont(QFont("Segoe UI", 8))
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #00cc66, stop:1 #008844);
|
||||||
pfl_indicator.setAlignment(Qt.AlignCenter)
|
|
||||||
pfl_indicator.setStyleSheet("color: #556; background: transparent; border: none;")
|
|
||||||
root.addWidget(pfl_indicator)
|
|
||||||
|
|
||||||
comp_frame = QFrame()
|
|
||||||
comp_frame.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background-color: #121318;
|
|
||||||
border: 1px solid #22242a;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
comp_layout = QVBoxLayout(comp_frame)
|
|
||||||
comp_layout.setContentsMargins(4, 4, 4, 4)
|
|
||||||
comp_layout.setSpacing(2)
|
|
||||||
|
|
||||||
comp_header = QHBoxLayout()
|
|
||||||
cl = QLabel("COMPRESSOR")
|
|
||||||
cl.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
|
||||||
cl.setStyleSheet("color: #778; background: transparent; border: none;")
|
|
||||||
comp_header.addWidget(cl)
|
|
||||||
|
|
||||||
self.comp_bypass = QPushButton("B")
|
|
||||||
self.comp_bypass.setFixedSize(22, 18)
|
|
||||||
self.comp_bypass.setCheckable(True)
|
|
||||||
self.comp_bypass.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
|
||||||
self.comp_bypass.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background-color: #22242a;
|
|
||||||
color: #778;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
QPushButton:checked {
|
QSlider::sub-page:vertical { background: transparent; border-radius: 2px; }
|
||||||
background-color: #4a1a1a;
|
""")
|
||||||
color: #ff6666;
|
meter_row.addWidget(self.volume_slider, stretch=1)
|
||||||
border: 1px solid #ff4444;
|
|
||||||
|
root.addLayout(meter_row, stretch=1)
|
||||||
|
|
||||||
|
self.volume_slider.valueChanged.connect(self._on_volume_changed)
|
||||||
|
|
||||||
|
def _on_volume_changed(self, value: int):
|
||||||
|
vol = (value / 100.0) ** 2
|
||||||
|
self._master.volume = vol
|
||||||
|
|
||||||
|
def _refresh(self):
|
||||||
|
self.vu_meter.set_value(self._master.vu_peak)
|
||||||
|
self.volume_slider.setValue(int(math.sqrt(self._master.volume) * 100))
|
||||||
|
|
||||||
|
class EffectSection(QFrame):
|
||||||
|
def __init__(self, engine, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.engine = engine
|
||||||
|
self._master = engine.mixer['master']
|
||||||
|
self._knobs = {}
|
||||||
|
|
||||||
|
self.setFixedWidth(110)
|
||||||
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||||
|
self.setStyleSheet("""
|
||||||
|
EffectSection {
|
||||||
|
background-color: #181a1e;
|
||||||
|
border: 1px solid #282a30;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
self.comp_bypass.setToolTip("Bypass compressor")
|
|
||||||
comp_header.addWidget(self.comp_bypass)
|
|
||||||
comp_layout.addLayout(comp_header)
|
|
||||||
|
|
||||||
knobs = QHBoxLayout()
|
self._build_ui()
|
||||||
knobs.setSpacing(4)
|
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.setInterval(30)
|
||||||
|
self.timer.timeout.connect(self._refresh)
|
||||||
|
self.timer.start()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
root = QVBoxLayout(self)
|
||||||
|
root.setContentsMargins(3, 6, 3, 6)
|
||||||
|
root.setSpacing(2)
|
||||||
|
|
||||||
|
header = QLabel("FX")
|
||||||
|
header.setFont(QFont("Segoe UI", 8, QFont.Bold))
|
||||||
|
header.setAlignment(Qt.AlignCenter)
|
||||||
|
header.setStyleSheet("color: #556670; background: transparent; border: none;")
|
||||||
|
root.addWidget(header)
|
||||||
|
|
||||||
|
lufs_frame = QFrame()
|
||||||
|
lufs_frame.setStyleSheet("QFrame { background-color: #121318; border: 1px solid #22242a; border-radius: 4px; }")
|
||||||
|
lufs_layout = QVBoxLayout(lufs_frame)
|
||||||
|
lufs_layout.setContentsMargins(3, 4, 3, 4)
|
||||||
|
lufs_layout.setSpacing(2)
|
||||||
|
|
||||||
|
self.lufs_value = QLabel("-70.0")
|
||||||
|
self.lufs_value.setFont(QFont("Segoe UI", 13, QFont.Bold))
|
||||||
|
self.lufs_value.setAlignment(Qt.AlignCenter)
|
||||||
|
self.lufs_value.setStyleSheet("color: #00cc88; background: transparent; border: none;")
|
||||||
|
lufs_layout.addWidget(self.lufs_value)
|
||||||
|
|
||||||
|
lufs_unit = QLabel("LUFS")
|
||||||
|
lufs_unit.setFont(QFont("Segoe UI", 6))
|
||||||
|
lufs_unit.setAlignment(Qt.AlignCenter)
|
||||||
|
lufs_unit.setStyleSheet("color: #556; background: transparent; border: none;")
|
||||||
|
lufs_layout.addWidget(lufs_unit)
|
||||||
|
|
||||||
|
self.pfl_indicator = QLabel("PFL\n—")
|
||||||
|
self.pfl_indicator.setFont(QFont("Segoe UI", 5))
|
||||||
|
self.pfl_indicator.setAlignment(Qt.AlignCenter)
|
||||||
|
self.pfl_indicator.setStyleSheet("color: #556; background: transparent; border: none;")
|
||||||
|
lufs_layout.addWidget(self.pfl_indicator)
|
||||||
|
|
||||||
|
root.addWidget(lufs_frame)
|
||||||
|
|
||||||
|
knob_col = QVBoxLayout()
|
||||||
|
knob_col.setSpacing(1)
|
||||||
|
|
||||||
|
bypass_row = QHBoxLayout()
|
||||||
|
bypass_row.setSpacing(2)
|
||||||
|
for label, btn_ref, obj, attr in [
|
||||||
|
("C", "comp_bypass", self._master.compressor, 'bypassed'),
|
||||||
|
("L", "lim_bypass", self._master.limiter, 'bypassed'),
|
||||||
|
]:
|
||||||
|
btn = QPushButton(label)
|
||||||
|
btn.setFixedSize(24, 18)
|
||||||
|
btn.setCheckable(True)
|
||||||
|
btn.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||||
|
btn.setStyleSheet("""
|
||||||
|
QPushButton { background-color: #22242a; color: #889; border: 1px solid #333; border-radius: 3px; }
|
||||||
|
QPushButton:checked { background-color: #4a1a1a; color: #ff6666; border: 1px solid #ff4444; }
|
||||||
|
""")
|
||||||
|
setattr(self, btn_ref, btn)
|
||||||
|
btn.toggled.connect(lambda c, o=obj, a=attr: setattr(o, a, c))
|
||||||
|
bypass_row.addWidget(btn)
|
||||||
|
knob_col.addLayout(bypass_row)
|
||||||
|
|
||||||
for label, attr, default, lo, hi, suffix in [
|
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"),
|
||||||
("ATK", "attack_ms", 5, 0.1, 50, "ms"),
|
("ATK", "attack_ms", 5, 0.1, 50, "ms"),
|
||||||
("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"),
|
||||||
|
("CEIL", "ceiling_db", -0.1, -10, 0, "dB"),
|
||||||
]:
|
]:
|
||||||
k, dial, val_label = self._make_knob(label, self._master.compressor, attr,
|
obj = self._master.compressor if attr != "ceiling_db" else self._master.limiter
|
||||||
default, lo, hi, suffix)
|
k, dial, val_label = self._make_knob(label, obj, attr, default, lo, hi, suffix)
|
||||||
knobs.addWidget(k)
|
knob_col.addWidget(k)
|
||||||
self._knobs[('compressor', attr)] = (dial, val_label, lo, hi, suffix)
|
self._knobs[('compressor' if attr != 'ceiling_db' else 'limiter', attr)] = (dial, val_label, lo, hi, suffix)
|
||||||
comp_layout.addLayout(knobs)
|
root.addLayout(knob_col)
|
||||||
root.addWidget(comp_frame)
|
|
||||||
|
|
||||||
lim_frame = QFrame()
|
|
||||||
lim_frame.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background-color: #121318;
|
|
||||||
border: 1px solid #22242a;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
lim_layout = QVBoxLayout(lim_frame)
|
|
||||||
lim_layout.setContentsMargins(4, 4, 4, 4)
|
|
||||||
lim_layout.setSpacing(2)
|
|
||||||
|
|
||||||
lim_header = QHBoxLayout()
|
|
||||||
ll = QLabel("LIMITER")
|
|
||||||
ll.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
|
||||||
ll.setStyleSheet("color: #778; background: transparent; border: none;")
|
|
||||||
lim_header.addWidget(ll)
|
|
||||||
|
|
||||||
self.lim_bypass = QPushButton("B")
|
|
||||||
self.lim_bypass.setFixedSize(22, 18)
|
|
||||||
self.lim_bypass.setCheckable(True)
|
|
||||||
self.lim_bypass.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
|
||||||
self.lim_bypass.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background-color: #22242a;
|
|
||||||
color: #778;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
QPushButton:checked {
|
|
||||||
background-color: #4a1a1a;
|
|
||||||
color: #ff6666;
|
|
||||||
border: 1px solid #ff4444;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.lim_bypass.setToolTip("Bypass limiter")
|
|
||||||
lim_header.addWidget(self.lim_bypass)
|
|
||||||
lim_layout.addLayout(lim_header)
|
|
||||||
|
|
||||||
lim_knobs = QHBoxLayout()
|
|
||||||
lim_knobs.setSpacing(4)
|
|
||||||
k, dial, val_label = self._make_knob("CEIL", self._master.limiter, "ceiling_db",
|
|
||||||
-0.1, -10, 0, "dB")
|
|
||||||
lim_knobs.addWidget(k)
|
|
||||||
self._knobs[('limiter', 'ceiling_db')] = (dial, val_label, -10, 0, "dB")
|
|
||||||
lim_layout.addLayout(lim_knobs)
|
|
||||||
root.addWidget(lim_frame)
|
|
||||||
root.addStretch()
|
|
||||||
|
|
||||||
def _make_knob(self, label, obj, attr, default, lo, hi, suffix):
|
def _make_knob(self, label, obj, attr, default, lo, hi, suffix):
|
||||||
frame = QFrame()
|
frame = QFrame()
|
||||||
frame.setStyleSheet("background: transparent; border: none;")
|
frame.setStyleSheet("background: transparent; border: none;")
|
||||||
layout = QVBoxLayout(frame)
|
layout = QHBoxLayout(frame)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
layout.setSpacing(1)
|
layout.setSpacing(2)
|
||||||
|
|
||||||
lbl = QLabel(label)
|
|
||||||
lbl.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
|
||||||
lbl.setAlignment(Qt.AlignCenter)
|
|
||||||
lbl.setStyleSheet("color: #556; background: transparent; border: none;")
|
|
||||||
layout.addWidget(lbl)
|
|
||||||
|
|
||||||
dial = QDial()
|
dial = QDial()
|
||||||
dial.setFixedSize(44, 44)
|
dial.setFixedSize(30, 30)
|
||||||
dial.setRange(0, 1000)
|
dial.setRange(0, 1000)
|
||||||
dial.setNotchesVisible(False)
|
dial.setNotchesVisible(False)
|
||||||
dial.setStyleSheet("""
|
dial.setStyleSheet("""
|
||||||
QDial {
|
QDial { background-color: #121318; border: 1px solid #2a2c33; border-radius: 15px; }
|
||||||
background-color: #121318;
|
|
||||||
border: 1px solid #2a2c33;
|
|
||||||
border-radius: 22px;
|
|
||||||
}
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def _val_to_pos(v):
|
def _val_to_pos(v):
|
||||||
@@ -465,10 +458,15 @@ class MasterSection(QFrame):
|
|||||||
|
|
||||||
dial.setValue(_val_to_pos(default))
|
dial.setValue(_val_to_pos(default))
|
||||||
|
|
||||||
|
lbl = QLabel(label)
|
||||||
|
lbl.setFont(QFont("Segoe UI", 8, QFont.Bold))
|
||||||
|
lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
lbl.setStyleSheet("color: #889; background: transparent; border: none;")
|
||||||
|
|
||||||
val_label = QLabel(f"{default}{suffix}")
|
val_label = QLabel(f"{default}{suffix}")
|
||||||
val_label.setFont(QFont("Segoe UI", 10, QFont.Bold))
|
val_label.setFont(QFont("Segoe UI", 7))
|
||||||
val_label.setAlignment(Qt.AlignCenter)
|
val_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
val_label.setStyleSheet("color: #ccd; background: transparent; border: none;")
|
val_label.setStyleSheet("color: #667; background: transparent; border: none;")
|
||||||
|
|
||||||
def _on_dial(v):
|
def _on_dial(v):
|
||||||
val = _pos_to_val(v)
|
val = _pos_to_val(v)
|
||||||
@@ -488,53 +486,29 @@ class MasterSection(QFrame):
|
|||||||
val_label.setText(f"{val}{suffix}")
|
val_label.setText(f"{val}{suffix}")
|
||||||
|
|
||||||
dial.valueChanged.connect(_on_dial)
|
dial.valueChanged.connect(_on_dial)
|
||||||
layout.addWidget(dial, alignment=Qt.AlignCenter)
|
layout.addWidget(dial)
|
||||||
|
layout.addWidget(lbl, 1)
|
||||||
layout.addWidget(val_label)
|
layout.addWidget(val_label)
|
||||||
return frame, dial, val_label
|
return frame, dial, val_label
|
||||||
|
|
||||||
def _connect_signals(self):
|
|
||||||
self.comp_bypass.toggled.connect(
|
|
||||||
lambda c: setattr(self._master.compressor, 'bypassed', c)
|
|
||||||
)
|
|
||||||
self.lim_bypass.toggled.connect(
|
|
||||||
lambda c: setattr(self._master.limiter, 'bypassed', c)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _refresh(self):
|
def _refresh(self):
|
||||||
self.vu_meter.set_value(self._master.vu_peak)
|
for (section, attr), (dial, val_label, lo, hi, suffix) in self._knobs.items():
|
||||||
self.lufs_label.setText(f"{self._master.lufs_current:.1f} LUFS")
|
obj = self._master.compressor if section == 'compressor' else self._master.limiter
|
||||||
|
val = getattr(obj, attr, lo)
|
||||||
|
dial.blockSignals(True)
|
||||||
|
dial.setValue(int((val - lo) / (hi - lo) * 1000))
|
||||||
|
dial.blockSignals(False)
|
||||||
|
val_label.setText(f"{val}{suffix}")
|
||||||
|
|
||||||
|
self.lufs_value.setText(f"{self._master.lufs_current:.1f}")
|
||||||
active_pfl = [ch for ch in self.engine.mixer['channels'] if ch.pfl]
|
active_pfl = [ch for ch in self.engine.mixer['channels'] if ch.pfl]
|
||||||
if active_pfl:
|
if active_pfl:
|
||||||
names = ", ".join(ch.name[:6] for ch in active_pfl)
|
names = ", ".join(ch.name[:3] for ch in active_pfl)
|
||||||
self.pfl_indicator.setText(f"PFL: {names}")
|
self.pfl_indicator.setText(f"PFL\n{names}")
|
||||||
self.pfl_indicator.setStyleSheet(
|
self.pfl_indicator.setStyleSheet("color: #00aaff; background: transparent; border: none;")
|
||||||
"color: #00aaff; background: transparent; border: none;"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.pfl_indicator.setText("PFL: —")
|
self.pfl_indicator.setText("PFL\n—")
|
||||||
self.pfl_indicator.setStyleSheet(
|
self.pfl_indicator.setStyleSheet("color: #556; background: transparent; border: none;")
|
||||||
"color: #556; background: transparent; border: none;"
|
|
||||||
)
|
|
||||||
|
|
||||||
for (section, attr), (dial, val_label, lo, hi, suffix) in self._knobs.items():
|
|
||||||
obj = self._master.compressor if section == 'compressor' else self._master.limiter
|
|
||||||
val = getattr(obj, attr, lo)
|
|
||||||
dial.blockSignals(True)
|
|
||||||
dial.setValue(int((val - lo) / (hi - lo) * 1000))
|
|
||||||
dial.blockSignals(False)
|
|
||||||
val_label.setText(f"{val}{suffix}")
|
|
||||||
|
|
||||||
for (section, attr), (dial, val_label, lo, hi, suffix) in self._knobs.items():
|
|
||||||
obj = self._master.compressor if section == 'compressor' else self._master.limiter
|
|
||||||
val = getattr(obj, attr, lo)
|
|
||||||
dial.blockSignals(True)
|
|
||||||
dial.setValue(int((val - lo) / (hi - lo) * 1000))
|
|
||||||
dial.blockSignals(False)
|
|
||||||
val_label.setText(f"{val}{suffix}")
|
|
||||||
|
|
||||||
def apply_theme(self, theme: dict):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MixerWidget(QFrame):
|
class MixerWidget(QFrame):
|
||||||
def __init__(self, engine, parent=None):
|
def __init__(self, engine, parent=None):
|
||||||
@@ -597,8 +571,11 @@ class MixerWidget(QFrame):
|
|||||||
self._strips.append(strip)
|
self._strips.append(strip)
|
||||||
channels_layout.addWidget(strip)
|
channels_layout.addWidget(strip)
|
||||||
|
|
||||||
self.master_section = MasterSection(self.engine)
|
self.master_pre = MasterPreSection(self.engine)
|
||||||
channels_layout.addWidget(self.master_section)
|
channels_layout.addWidget(self.master_pre)
|
||||||
|
|
||||||
|
self.effects = EffectSection(self.engine)
|
||||||
|
channels_layout.addWidget(self.effects)
|
||||||
|
|
||||||
scroll.setWidget(scroll_content)
|
scroll.setWidget(scroll_content)
|
||||||
root.addWidget(scroll)
|
root.addWidget(scroll)
|
||||||
@@ -606,7 +583,8 @@ class MixerWidget(QFrame):
|
|||||||
def apply_theme(self, theme: dict):
|
def apply_theme(self, theme: dict):
|
||||||
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_pre.apply_theme(theme)
|
||||||
|
self.effects.apply_theme(theme)
|
||||||
|
|
||||||
def refresh_strips(self):
|
def refresh_strips(self):
|
||||||
for strip in self._strips:
|
for strip in self._strips:
|
||||||
|
|||||||
+2
-5
@@ -6,15 +6,13 @@ from PySide6.QtWidgets import (
|
|||||||
QListWidget, QListWidgetItem, QAbstractItemView,
|
QListWidget, QListWidgetItem, QAbstractItemView,
|
||||||
QMenu, QSplitter
|
QMenu, QSplitter
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QTimer, QSize, QSize
|
from PySide6.QtCore import Qt, QTimer, QSize
|
||||||
from PySide6.QtGui import QFont, QColor, QAction
|
from PySide6.QtGui import QFont, QColor, QAction
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from icons import icon_save, icon_load
|
from icons import icon_save, icon_load
|
||||||
|
|
||||||
from icons import icon_save, icon_load
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────
|
# ─────────────────────────────────────────
|
||||||
# PLAYLIST ITEM
|
# PLAYLIST ITEM
|
||||||
@@ -277,8 +275,7 @@ class BasePlaylistPanel(QWidget):
|
|||||||
paths.append(item.filepath)
|
paths.append(item.filepath)
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
def clear_list(self, lst: QListWidget):
|
|
||||||
lst.clear()
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────
|
# ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ class SettingsPopup(QFrame):
|
|||||||
("Master REL", "master_control", "release_ms"),
|
("Master REL", "master_control", "release_ms"),
|
||||||
("Master MKG", "master_control", "makeup_gain_db"),
|
("Master MKG", "master_control", "makeup_gain_db"),
|
||||||
("Master CEIL", "master_control", "ceiling_db"),
|
("Master CEIL", "master_control", "ceiling_db"),
|
||||||
|
("Master Volume", "master_volume", "master"),
|
||||||
]
|
]
|
||||||
|
|
||||||
self._learn_btns = {}
|
self._learn_btns = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user