2026-04-30 00:30:23 +10:00
|
|
|
# lufs_meter_widget.py
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
from PySide6.QtWidgets import QFrame, QVBoxLayout, QLabel, QWidget
|
|
|
|
|
from PySide6.QtCore import Qt, QTimer
|
|
|
|
|
from PySide6.QtGui import QFont, QPainter, QColor, QLinearGradient, QPen
|
2026-04-30 00:30:23 +10:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class LUFSMeterWidget(QFrame):
|
2026-05-08 17:19:11 +10:00
|
|
|
"""Vertical LUFS meter with -14 LUFS target for streaming/broadcast."""
|
2026-04-30 00:30:23 +10:00
|
|
|
|
|
|
|
|
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)
|
2026-05-08 17:19:11 +10:00
|
|
|
self.timer.timeout.connect(self._update_meter)
|
2026-04-30 00:30:23 +10:00
|
|
|
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)
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
self.meter_canvas = MeterCanvas(self.engine)
|
2026-04-30 00:30:23 +10:00
|
|
|
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)
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
def _update_meter(self):
|
2026-04-30 00:30:23 +10:00
|
|
|
lufs = self.engine.lufs_current
|
|
|
|
|
self.readout.setText(f"{lufs:.1f}")
|
2026-05-08 17:19:11 +10:00
|
|
|
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)
|
2026-04-30 00:30:23 +10:00
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
def paintEvent(self, event):
|
|
|
|
|
painter = QPainter(self)
|
|
|
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
2026-04-30 00:30:23 +10:00
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
rect = self.rect().adjusted(3, 3, -3, -3)
|
|
|
|
|
lufs = self.engine.lufs_current
|
2026-04-30 00:30:23 +10:00
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
# Scale: -50 LUFS (bottom) to 0 (top) - makes -14 more readable
|
|
|
|
|
lufs_min = -50
|
|
|
|
|
lufs_max = 0
|
|
|
|
|
lufs_range = lufs_max - lufs_min
|
2026-04-30 00:30:23 +10:00
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
lufs = max(lufs_min, min(lufs_max, lufs))
|
|
|
|
|
fill_pct = (lufs - lufs_min) / lufs_range
|
2026-04-30 00:30:23 +10:00
|
|
|
fill_h = int(rect.height() * fill_pct)
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
# Color zones optimized for -14 LUFS target
|
|
|
|
|
gradient = QLinearGradient(0, rect.bottom(), 0, rect.top())
|
2026-04-30 00:30:23 +10:00
|
|
|
|
|
|
|
|
if lufs < -30:
|
2026-05-08 17:19:11 +10:00
|
|
|
# Very quiet - dark green
|
|
|
|
|
gradient.setColorAt(0, QColor("#002200"))
|
|
|
|
|
gradient.setColorAt(1, QColor("#004400"))
|
|
|
|
|
elif lufs < -18:
|
|
|
|
|
# Quiet - green
|
|
|
|
|
gradient.setColorAt(0, QColor("#00aa00"))
|
2026-04-30 00:30:23 +10:00
|
|
|
gradient.setColorAt(1, QColor("#00dd00"))
|
2026-05-08 17:19:11 +10:00
|
|
|
elif lufs < -12:
|
|
|
|
|
# Target zone -18 to -12 LUFS (±4 LU around -14) - bright green
|
|
|
|
|
gradient.setColorAt(0, QColor("#00ff00"))
|
2026-04-30 00:30:23 +10:00
|
|
|
gradient.setColorAt(1, QColor("#00ff00"))
|
2026-05-08 17:19:11 +10:00
|
|
|
elif lufs < -6:
|
|
|
|
|
# Getting hot - yellow
|
|
|
|
|
gradient.setColorAt(0, QColor("#ffcc00"))
|
2026-04-30 00:30:23 +10:00
|
|
|
gradient.setColorAt(1, QColor("#ffaa00"))
|
|
|
|
|
else:
|
2026-05-08 17:19:11 +10:00
|
|
|
# Too hot - red
|
|
|
|
|
gradient.setColorAt(0, QColor("#ff0000"))
|
2026-04-30 00:30:23 +10:00
|
|
|
gradient.setColorAt(1, QColor("#ff0000"))
|
|
|
|
|
|
|
|
|
|
painter.fillRect(
|
|
|
|
|
rect.x(),
|
2026-05-08 17:19:11 +10:00
|
|
|
rect.bottom() - fill_h,
|
2026-04-30 00:30:23 +10:00
|
|
|
rect.width(),
|
|
|
|
|
fill_h,
|
|
|
|
|
gradient
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
# 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()
|