Native integrated/LRA, background prefetch, timing-rich status

LUFS integrated + LRA:
- Reimplement BS.1770 two-stage gating (integrated) and EBU 3342 (LRA) directly
  from the cached K-weighted signal, dropping pyloudnorm's two whole-signal
  re-filters. Validated bit-equal to pyloudnorm across steady/dynamic/quiet
  signals (0.0000 diff). LUFS compute ~1.9s -> ~0.6s (orig 3.8s). pyloudnorm now
  only supplies the filter coefficients.

Prefetch on load:
- After a file loads, PrefetchWorker computes the remaining metrics in the
  background (sequential, cooperatively cancellable, skips on-demand hits), so
  the first switch to any metric is instant. Superseded when a new file loads.

Status slip:
- Workers measure compute time; metricTiming + phase/duration progress messages
  drive the slip ("Loaded in Ns - computing X...", "X computed in Ys"). The old
  fake percentage (10% then done) is logged but no longer shown.
- shutdown() stops background threads on window close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2026-06-14 01:49:50 +09:00
parent d7782bb9d9
commit 507af2f676
4 changed files with 241 additions and 61 deletions
+22 -2
View File
@@ -121,8 +121,14 @@ class MainWindow(QMainWindow):
self.analysis_manager.metricComputeStarted.connect(self.on_metric_compute_started)
self.analysis_manager.metricReady.connect(self.on_metric_ready)
self.analysis_manager.metricComputeError.connect(self.on_metric_compute_error)
self.analysis_manager.metricTiming.connect(self.on_metric_timing)
self.visualization_widget.referenceLineMoved.connect(self.on_reference_line_moved)
def closeEvent(self, event):
"""Stop background analysis/prefetch threads before the window closes."""
self.analysis_manager.shutdown()
super().closeEvent(event)
def dragEnterEvent(self, event):
"""Handle drag enter event for file drops."""
if event.mimeData().hasUrls():
@@ -199,9 +205,13 @@ class MainWindow(QMainWindow):
self.visualization_widget.set_status(f"Error analyzing {filename}: {error_message}")
def on_progress_update(self, message, percentage):
"""Called when analysis progress updates."""
"""Called when analysis progress updates.
The messages already carry phase + timing; the percentage was a coarse
fake (load jumped 10->done), so it's logged but not shown in the slip.
"""
self.logger.debug(f"Progress: {message} ({percentage}%)")
self.visualization_widget.set_status(f"{message} ({percentage}%)")
self.visualization_widget.set_status(message)
def on_file_selected(self, item):
"""Called when a file is highlighted (drives the metadata panel only)."""
@@ -296,6 +306,16 @@ class MainWindow(QMainWindow):
return # no longer part of the overlay set
self._refresh_view()
def on_metric_timing(self, file_path: str, metric_id: str, seconds: float):
"""An on-demand metric compute finished — report how long it took."""
if file_path not in self._overlay_paths():
return
if metric_id != self.plot_control.current_metric_id():
return
metric = METRICS.get(metric_id)
display = metric.display_name if metric else metric_id
self.visualization_widget.set_status(f"{display} computed in {seconds:.1f}s")
def on_metric_compute_error(self, file_path: str, metric_id: str, error_message: str):
self.logger.error(f"Metric compute failed ({metric_id} / {os.path.basename(file_path)}): {error_message}")
if file_path in self._overlay_paths():