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:
2026-05-13 16:47:02 +10:00
parent d2240c61c3
commit 7de0cc3de0
8 changed files with 1117 additions and 533 deletions
+206 -69
View File
@@ -5,7 +5,7 @@ from PySide6.QtWidgets import (
QPushButton, QFrame, QSizePolicy
)
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 os
@@ -16,8 +16,6 @@ from icons import (
class WaveformWidget(QFrame):
"""Displays downsampled waveform with playhead position."""
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedHeight(60)
@@ -25,7 +23,7 @@ class WaveformWidget(QFrame):
QFrame {
background-color: #050505;
border: 1px solid #1a1a1a;
border-radius: 3px;
border-radius: 4px;
}
""")
@@ -41,7 +39,6 @@ class WaveformWidget(QFrame):
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
@@ -72,7 +69,6 @@ class WaveformWidget(QFrame):
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):
@@ -81,7 +77,6 @@ class WaveformWidget(QFrame):
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:
@@ -101,6 +96,80 @@ class WaveformWidget(QFrame):
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):
def __init__(self, parent=None):
super().__init__(parent)
@@ -119,9 +188,7 @@ class LCDDisplay(QFrame):
self.track_label = QLabel("NO TRACK LOADED")
self.track_label.setFont(QFont("Courier New", 13, QFont.Bold))
self.track_label.setStyleSheet(
"color: #00ff41; background: transparent;"
)
self.track_label.setStyleSheet("color: #00ff41; background: transparent;")
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.track_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
@@ -130,16 +197,12 @@ class LCDDisplay(QFrame):
self.elapsed_label = QLabel("00:00")
self.elapsed_label.setFont(QFont("Courier New", 32, QFont.Bold))
self.elapsed_label.setStyleSheet(
"color: #00ff41; background: transparent;"
)
self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent;")
self.elapsed_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.remaining_label = QLabel("-00:00")
self.remaining_label.setFont(QFont("Courier New", 18, QFont.Bold))
self.remaining_label.setStyleSheet(
"color: #00cc33; background: transparent;"
)
self.remaining_label.setStyleSheet("color: #00cc33; background: transparent;")
self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
time_row.addWidget(self.elapsed_label)
@@ -150,15 +213,11 @@ class LCDDisplay(QFrame):
self.status_label = QLabel("STOPPED")
self.status_label.setFont(QFont("Courier New", 11, QFont.Bold))
self.status_label.setStyleSheet(
"color: #008822; background: transparent;"
)
self.status_label.setStyleSheet("color: #008822; background: transparent;")
self.queue_label = QLabel("QUEUE: EMPTY")
self.queue_label.setFont(QFont("Courier New", 10))
self.queue_label.setStyleSheet(
"color: #008822; background: transparent;"
)
self.queue_label.setStyleSheet("color: #008822; background: transparent;")
self.queue_label.setAlignment(Qt.AlignRight)
status_row.addWidget(self.status_label)
@@ -190,33 +249,19 @@ class LCDDisplay(QFrame):
def set_playing(self, playing: bool):
colour = "#00ff41" if playing else "#00aa2a"
self.elapsed_label.setStyleSheet(
f"color: {colour}; background: transparent;"
)
self.elapsed_label.setStyleSheet(f"color: {colour}; background: transparent;")
def flash_warning(self, active: bool):
"""Flash all time displays red when in warning zone."""
if active:
self.elapsed_label.setStyleSheet(
"color: #ff4444; background: transparent;"
)
self.remaining_label.setStyleSheet(
"color: #ff4444; background: transparent;"
)
self.elapsed_label.setStyleSheet("color: #ff4444; background: transparent;")
self.remaining_label.setStyleSheet("color: #ff4444; background: transparent;")
else:
self.elapsed_label.setStyleSheet(
"color: #00ff41; background: transparent;"
)
self.remaining_label.setStyleSheet(
"color: #00cc33; background: transparent;"
)
self.elapsed_label.setStyleSheet("color: #00ff41; background: transparent;")
self.remaining_label.setStyleSheet("color: #00cc33; background: transparent;")
def make_icon_button(icon: QIcon,
color: str = "#3a3a3a",
icon_color: str = "white",
width: int = 52,
height: int = 48) -> QPushButton:
def make_icon_button(icon, color="#3a3a3a", icon_color="white",
width=52, height=48):
btn = QPushButton()
btn.setFixedSize(width, height)
btn.setIconSize(QSize(22, 22))
@@ -250,9 +295,7 @@ class DeckWidget(QWidget):
self.current_theme = None
self.flash_state = False
self._playing_icon = False
self._playing_icon = False
# Convert "DECK 1" to "DECK_001"
deck_num = label.split()[-1]
self.deck_label = f"DECK_{int(deck_num):03d}"
@@ -321,10 +364,9 @@ class DeckWidget(QWidget):
def apply_theme(self, theme: dict):
self.current_theme = theme
border_color = theme['border']
bg_color = theme['bg2']
self.setStyleSheet(f"""
QWidget {{
background-color: {bg_color};
@@ -332,7 +374,6 @@ class DeckWidget(QWidget):
border-radius: 6px;
}}
""")
self.lcd.setStyleSheet(f"""
QFrame {{
background-color: {theme['bg']};
@@ -340,7 +381,6 @@ class DeckWidget(QWidget):
border-radius: 4px;
}}
""")
self.waveform.setStyleSheet(f"""
QFrame {{
background-color: {theme['bg']};
@@ -466,7 +506,6 @@ class DeckWidget(QWidget):
def _refresh_display(self):
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 deck.position >= deck.track.num_samples - 1:
if deck.queued:
@@ -477,7 +516,6 @@ class DeckWidget(QWidget):
self.lcd.update_queue("")
self.lcd.update_status("LOADED")
# Sync button if stopped manually
if not deck.playing and self._playing_icon:
self.btn_play.setIcon(icon_play("#00ff41"))
self._playing_icon = False
@@ -493,33 +531,21 @@ class DeckWidget(QWidget):
self.lcd.set_playing(False)
if deck.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
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)
# Flash header AND all numbers if in warning zone and playing
if deck.playing and in_warning:
flash_color = '#ff4444' if self.flash_state else self.accent_color
# Flash header
self.header.setStyleSheet(f"""
color: {flash_color};
background: transparent;
border: none;
padding: 4px;
""")
# Flash LCD numbers
self.lcd.flash_warning(self.flash_state)
else:
# Normal colors
self.header.setStyleSheet(f"""
color: {self.accent_color};
background: transparent;
@@ -527,10 +553,121 @@ class DeckWidget(QWidget):
padding: 4px;
""")
self.lcd.flash_warning(False)
self.waveform.set_position(position_pct, in_warning and self.flash_state)
if deck.queued:
self.lcd.update_queue(
os.path.basename(deck.queued.filepath)
)
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;
}}
""")