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
+14 -6
View File
@@ -15,7 +15,7 @@ Developed with Claude Code assistance.
### Roadmap
See [CLAUDE.md](CLAUDE.md) for the full development roadmap. Near-term:
plot-control widget cluster, LUFS, dynamic range, interactive axis controls.
dynamic range, plot-style controls, interactive axis controls.
## Quick start
@@ -40,19 +40,27 @@ selector. The directory is gitignored to avoid bundling licensed font data.
See [CJK_FONTS.md](CJK_FONTS.md) for details.
## Dependencies
`librosa`, `numpy`, `matplotlib`, `mutagen`, `PyQt5` — all pinned through
`uv.lock`. Python 3.10+.
`librosa`, `numpy`, `matplotlib`, `mutagen`, `pyloudnorm`, `PyQt5` — all pinned
through `uv.lock`. Python 3.10+.
## Architecture
| Module | Responsibility |
| --- | --- |
| `main.py` | `MainWindow` + the `ujm` entry point |
| `analysis_results_manager.py` | Background `QThread` worker, result cache |
| `analysis_results_manager.py` | Background `QThread` worker, result + metric-data cache |
| `master_core.py` | `AudioFile`: librosa loading, RMS rolling window, BPM |
| `plotting_engine.py` | Matplotlib `Figure` builder for the power graph |
| `metrics.py` | Pluggable `Metric` ABC + registry (RMS Power, Waveform, …) |
| `audio_visualization_widget.py` | Embedded `FigureCanvasQTAgg` host |
| `font_manager.py` | Custom + system CJK font discovery, matplotlib/Qt config |
| `font_control_widget.py` | Font picker, size slider, refresh-plot button |
| `font_control_widget.py` | Font picker + size slider |
| `plot_control_widget.py` | Metric selector + refresh-plot button |
| `logger_setup.py` | CLI log-level parsing + custom TRACE level |
| `setup_fonts.py` | Diagnostic utility (run standalone) |
### Adding a metric
Subclass `Metric` in `metrics.py`, implement `compute(audio_file) -> data` (the
heavy part, runs on the worker thread) and `render(data, file_path) -> Figure`
(cheap, runs on the GUI thread). Register the instance in the `METRICS` dict at
the bottom of the file — it shows up in the dropdown automatically.