Add pluggable metrics architecture with LUFS

Introduce a Metric ABC (compute on worker thread, render on GUI thread)
with a METRICS registry, and refactor the analysis pipeline around it.
RMS power (ported), raw waveform, and BS.1770 LUFS (via pyloudnorm) ship
as the initial three; new metrics drop in by appending to METRICS.

- metrics.py: Metric ABC + RMSPowerMetric, WaveformMetric (locked to
  +/-1.1 y-range for float headroom), LUFSMetric (short-term 3 s window
  + integrated value, with streaming-target reference line).
- plot_control_widget.py: metric selector dropdown + Refresh Plot button
  (moved out of FontControlWidget).
- analysis_results_manager.py: AnalysisResult caches the AudioFile and a
  per-metric data dict; new MetricComputeWorker runs metric switches off
  the GUI thread via metricComputeStarted/metricReady/metricComputeError
  signals, so LUFS on a 12-minute track no longer stalls the UI.
- main.py: all redraw paths funnel through one _render_or_request helper;
  stale-result guards keep slow computes from overwriting fresh selections.
- plotting_engine.py removed (metadata text moved onto AnalysisResult;
  figure construction lives in each Metric).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2026-05-30 00:42:45 +09:00
parent cf901a5686
commit 7bdf465799
10 changed files with 654 additions and 334 deletions
+4 -29
View File
@@ -5,8 +5,8 @@ Self-contained widget for easy layout management.
import logging
from typing import List, Dict, Optional
from PyQt5.QtWidgets import (QWidget, QComboBox, QVBoxLayout, QHBoxLayout,
QLabel, QSlider, QPushButton, QGroupBox)
from PyQt5.QtWidgets import (QWidget, QComboBox, QVBoxLayout, QHBoxLayout,
QLabel, QSlider, QGroupBox)
from PyQt5.QtCore import pyqtSignal, Qt
from font_manager import get_font_manager
@@ -19,13 +19,11 @@ class FontControlWidget(QWidget):
Provides clustered interface for:
- Font selection (unified for Qt and matplotlib)
- Qt font size adjustment
- Plot regeneration controls
"""
# Signals
fontChanged = pyqtSignal(str, str) # (font_name, font_type)
fontSizeChanged = pyqtSignal(int) # font_size
plotRefreshRequested = pyqtSignal() # manual refresh request
def __init__(self, parent=None):
"""Initialize the font control widget."""
@@ -59,11 +57,7 @@ class FontControlWidget(QWidget):
# Font size section
size_section = self._create_font_size_section()
group_layout.addWidget(size_section)
# Control buttons section
button_section = self._create_button_section()
group_layout.addWidget(button_section)
layout.addWidget(group_box)
def _create_font_selector_section(self) -> QWidget:
@@ -119,20 +113,6 @@ class FontControlWidget(QWidget):
return section
def _create_button_section(self) -> QWidget:
"""Create the control buttons section."""
section = QWidget()
layout = QHBoxLayout(section)
layout.setContentsMargins(0, 0, 0, 0)
# Refresh plot button
self.refresh_button = QPushButton("Refresh Plot")
self.refresh_button.setToolTip("Regenerate current plot with new font settings")
self.refresh_button.clicked.connect(self.on_refresh_plot_clicked)
layout.addWidget(self.refresh_button)
return section
def refresh_font_list(self):
"""Refresh the list of available fonts."""
self.logger.debug("Refreshing font list...")
@@ -243,11 +223,6 @@ class FontControlWidget(QWidget):
# Emit signal for external listeners
self.fontSizeChanged.emit(size)
def on_refresh_plot_clicked(self):
"""Handle manual plot refresh button click."""
self.logger.info("Manual plot refresh requested")
self.plotRefreshRequested.emit()
def _apply_font_change(self, font_name: str, font_type: str):
"""Apply the font change to both Qt and matplotlib."""
try: