Move plotting to pyqtgraph: interactive, overlay-capable render layer
Replace the fire-and-forget matplotlib pipeline (render() -> throwaway Figure -> canvas teardown) with a three-stage architecture that supports zoom/pan, lin/log toggling, and multi-file overlay: compute(audio_file) -> data # heavy, worker thread, backend-neutral build_spec(data, view) -> PlotSpec # cheap, GUI thread, view-aware show_specs([(label, spec, color)]) # pyqtgraph, persistent PlotItem, overlay - plotspec.py: backend-agnostic descriptors (Curve, Band, HLine, Heatmap, AxisSpec, PlotSpec) + ViewState (recompute-free lin/log) - audio_visualization_widget.py: persistent pyqtgraph plot, never torn down; per-dataset colours for overlay; spectrogram log-freq via row resample (ImageItem is affine-only); ColorBarItem at a fixed cell - Compare/overlay driven by file-list checkboxes; stable per-song colour by row - Custom draggable reference lines (add/clear), persist across redraws - Axis-constrained scroll zoom: Ctrl=time, Shift=value (_AxisZoomViewBox) - RMS render no longer per-segment fill_between (was the slow path) Fixes found in review/testing: - FillBetweenItem needs penned child curves or it fills nothing (RMS/Waveform were blank); band fill verified by pixel count - band overlay alpha was a no-op (QBrush.color() returns a copy) - colorbar could stack across renders; now added/removed at a fixed layout cell Deferred (per scope): stereo retention, deep perf rewrites (eager beat_track, true-peak/crest loops, shared LUFS), per-song colour picker UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -214,22 +214,26 @@ class AnalysisResultsManager(QObject):
|
||||
self.metric_workers.pop((file_path, metric_id), None)
|
||||
self.metricComputeError.emit(file_path, metric_id, error_message)
|
||||
|
||||
def get_metric_figure(self, file_path: str, metric_id: str):
|
||||
"""Render a Figure from cached metric data. Returns None if not cached.
|
||||
def get_metric_data(self, file_path: str, metric_id: str):
|
||||
"""Return cached metric data, or None if not computed yet.
|
||||
|
||||
Never triggers compute — call `request_metric` first and listen for
|
||||
`metricReady` if you need on-demand computation.
|
||||
`metricReady` if you need on-demand computation. Spec/figure building is the
|
||||
GUI layer's job (it owns the view-state), so this stays render-agnostic.
|
||||
"""
|
||||
result = self.results_cache.get(file_path)
|
||||
if result is None:
|
||||
return None
|
||||
metric = METRICS.get(metric_id)
|
||||
if metric is None:
|
||||
if metric_id not in METRICS:
|
||||
return None
|
||||
data = result.metric_data.get(metric_id)
|
||||
if data is None:
|
||||
return None
|
||||
return metric.render(data, file_path)
|
||||
return result.metric_data.get(metric_id)
|
||||
|
||||
def display_label(self, file_path: str) -> str:
|
||||
"""Short human label for a file (song name if known, else basename)."""
|
||||
result = self.results_cache.get(file_path)
|
||||
if result is not None and result.song_name:
|
||||
return result.song_name
|
||||
return os.path.basename(file_path)
|
||||
|
||||
def get_metadata_text(self, file_path: str) -> str:
|
||||
result = self.results_cache.get(file_path)
|
||||
|
||||
Reference in New Issue
Block a user