2024-04-10 20:22:59 +09:00
|
|
|
import sys
|
2025-08-21 22:43:11 +09:00
|
|
|
import os
|
2025-08-21 23:39:53 +09:00
|
|
|
import logging
|
2025-08-21 22:43:11 +09:00
|
|
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
|
|
|
QHBoxLayout, QSplitter, QLabel, QListWidget,
|
2025-08-22 00:18:31 +09:00
|
|
|
QTextEdit, QListWidgetItem, QPushButton, QFileDialog)
|
2024-04-10 20:22:59 +09:00
|
|
|
from PyQt5.QtCore import Qt
|
|
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
from audio_visualization_widget import AudioVisualizationWidget, dataset_color
|
2025-08-21 22:43:11 +09:00
|
|
|
from analysis_results_manager import AnalysisResultsManager
|
2025-08-21 23:39:53 +09:00
|
|
|
from logger_setup import setup_logging, parse_log_args
|
|
|
|
|
from font_manager import initialize_fonts, get_font_manager
|
2025-08-22 00:18:31 +09:00
|
|
|
from font_control_widget import FontControlWidget
|
2026-05-30 00:42:45 +09:00
|
|
|
from plot_control_widget import PlotControlWidget
|
2026-06-14 00:51:33 +09:00
|
|
|
from ref_line_widget import RefLineControlWidget, RefLineDialog
|
2026-06-14 00:35:10 +09:00
|
|
|
from metrics import METRICS
|
2026-06-14 00:51:33 +09:00
|
|
|
from plotspec import RefLineProps, apply_x_mode
|
2025-08-21 22:43:11 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
|
|
|
"""Main application window with modular audio analysis interface."""
|
|
|
|
|
|
2024-04-10 20:22:59 +09:00
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__()
|
2025-08-21 23:39:53 +09:00
|
|
|
self.logger = logging.getLogger(__name__)
|
2025-08-21 22:43:11 +09:00
|
|
|
self.analysis_manager = AnalysisResultsManager()
|
2026-06-14 00:35:10 +09:00
|
|
|
# Guards programmatic list mutations from triggering re-render storms.
|
|
|
|
|
self._suppress_list_signals = False
|
2026-06-14 00:51:33 +09:00
|
|
|
# Custom reference lines, owned here and pushed to the plot each render.
|
|
|
|
|
self.ref_lines: list[RefLineProps] = []
|
2024-04-10 20:22:59 +09:00
|
|
|
self.initUI()
|
2025-08-21 22:43:11 +09:00
|
|
|
self.connect_signals()
|
2024-04-10 20:15:13 +09:00
|
|
|
|
2024-04-10 20:22:59 +09:00
|
|
|
def initUI(self):
|
2025-08-21 22:43:11 +09:00
|
|
|
"""Initialize the user interface."""
|
|
|
|
|
self.setWindowTitle('Audio Mastering Analysis Toolkit')
|
|
|
|
|
self.setGeometry(100, 100, 1200, 700)
|
2024-04-10 20:22:59 +09:00
|
|
|
self.setAcceptDrops(True)
|
|
|
|
|
|
2025-08-21 22:43:11 +09:00
|
|
|
# Create central widget with splitter
|
|
|
|
|
central_widget = QWidget()
|
|
|
|
|
self.setCentralWidget(central_widget)
|
|
|
|
|
|
|
|
|
|
# Main horizontal layout
|
|
|
|
|
main_layout = QHBoxLayout(central_widget)
|
|
|
|
|
splitter = QSplitter(Qt.Horizontal)
|
|
|
|
|
main_layout.addWidget(splitter)
|
|
|
|
|
|
|
|
|
|
# Left panel - File list and metadata
|
|
|
|
|
left_panel = self.create_left_panel()
|
|
|
|
|
splitter.addWidget(left_panel)
|
|
|
|
|
|
|
|
|
|
# Right panel - Visualization
|
|
|
|
|
self.visualization_widget = AudioVisualizationWidget()
|
|
|
|
|
splitter.addWidget(self.visualization_widget)
|
|
|
|
|
|
|
|
|
|
# Set splitter proportions
|
|
|
|
|
splitter.setSizes([300, 900]) # Left panel narrower than visualization
|
|
|
|
|
|
|
|
|
|
def create_left_panel(self):
|
|
|
|
|
"""Create the left panel with file list and metadata."""
|
|
|
|
|
panel = QWidget()
|
|
|
|
|
layout = QVBoxLayout(panel)
|
|
|
|
|
|
2025-08-22 00:18:31 +09:00
|
|
|
# Open File button
|
|
|
|
|
self.open_file_button = QPushButton("Open Audio File...")
|
|
|
|
|
self.open_file_button.clicked.connect(self.open_file_dialog)
|
|
|
|
|
layout.addWidget(self.open_file_button)
|
|
|
|
|
|
|
|
|
|
# Font control cluster
|
|
|
|
|
self.font_control = FontControlWidget()
|
|
|
|
|
self.font_control.fontChanged.connect(self.on_font_changed)
|
|
|
|
|
self.font_control.fontSizeChanged.connect(self.on_font_size_changed)
|
|
|
|
|
layout.addWidget(self.font_control)
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
# Plot control cluster (metric selector + scale toggle + refresh)
|
2026-05-30 00:42:45 +09:00
|
|
|
self.plot_control = PlotControlWidget()
|
|
|
|
|
self.plot_control.metricChanged.connect(self.on_metric_changed)
|
2026-06-14 00:35:10 +09:00
|
|
|
self.plot_control.viewChanged.connect(self.on_view_changed)
|
2026-05-30 00:42:45 +09:00
|
|
|
self.plot_control.plotRefreshRequested.connect(self.on_plot_refresh_requested)
|
|
|
|
|
layout.addWidget(self.plot_control)
|
2026-06-14 00:35:10 +09:00
|
|
|
|
2026-06-14 00:51:33 +09:00
|
|
|
# Reference-line management cluster (list + add/edit/remove/clear).
|
|
|
|
|
self.ref_line_control = RefLineControlWidget()
|
|
|
|
|
self.ref_line_control.addRequested.connect(self.on_add_reference_line)
|
|
|
|
|
self.ref_line_control.editRequested.connect(self.on_edit_reference_line)
|
|
|
|
|
self.ref_line_control.removeRequested.connect(self.on_remove_reference_line)
|
|
|
|
|
self.ref_line_control.clearRequested.connect(self.on_clear_reference_lines)
|
|
|
|
|
layout.addWidget(self.ref_line_control)
|
|
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
# File list. Each item carries a checkbox: the checked set is the overlay
|
|
|
|
|
# set drawn on the graph; the highlighted item drives the metadata panel.
|
|
|
|
|
self.file_list_label = QLabel("Analyzed Files (tick to overlay):")
|
2025-08-21 22:43:11 +09:00
|
|
|
layout.addWidget(self.file_list_label)
|
2026-06-14 00:35:10 +09:00
|
|
|
|
2025-08-21 22:43:11 +09:00
|
|
|
self.file_list = QListWidget()
|
|
|
|
|
self.file_list.itemClicked.connect(self.on_file_selected)
|
2026-06-14 00:35:10 +09:00
|
|
|
self.file_list.itemChanged.connect(self.on_file_check_changed)
|
2025-08-21 22:43:11 +09:00
|
|
|
layout.addWidget(self.file_list)
|
|
|
|
|
|
|
|
|
|
# Metadata display
|
|
|
|
|
self.metadata_label = QLabel("File Information:")
|
|
|
|
|
layout.addWidget(self.metadata_label)
|
|
|
|
|
|
|
|
|
|
self.metadata_display = QTextEdit()
|
|
|
|
|
self.metadata_display.setReadOnly(True)
|
|
|
|
|
self.metadata_display.setMaximumHeight(150)
|
|
|
|
|
layout.addWidget(self.metadata_display)
|
|
|
|
|
|
|
|
|
|
# Instructions
|
|
|
|
|
instructions = QLabel(
|
|
|
|
|
"Drag and drop audio files (.mp3, .wav) onto this window to analyze them."
|
|
|
|
|
)
|
|
|
|
|
instructions.setWordWrap(True)
|
|
|
|
|
instructions.setStyleSheet("color: gray; font-style: italic;")
|
|
|
|
|
layout.addWidget(instructions)
|
|
|
|
|
|
|
|
|
|
return panel
|
|
|
|
|
|
|
|
|
|
def connect_signals(self):
|
|
|
|
|
"""Connect analysis manager signals to GUI updates."""
|
|
|
|
|
self.analysis_manager.analysisStarted.connect(self.on_analysis_started)
|
|
|
|
|
self.analysis_manager.analysisCompleted.connect(self.on_analysis_completed)
|
|
|
|
|
self.analysis_manager.analysisError.connect(self.on_analysis_error)
|
2025-08-21 23:39:53 +09:00
|
|
|
self.analysis_manager.progressUpdate.connect(self.on_progress_update)
|
2026-05-30 00:42:45 +09:00
|
|
|
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)
|
2026-06-14 00:51:33 +09:00
|
|
|
self.visualization_widget.referenceLineMoved.connect(self.on_reference_line_moved)
|
2024-04-10 20:15:13 +09:00
|
|
|
|
2024-04-10 20:22:59 +09:00
|
|
|
def dragEnterEvent(self, event):
|
2025-08-21 22:43:11 +09:00
|
|
|
"""Handle drag enter event for file drops."""
|
2024-04-10 20:22:59 +09:00
|
|
|
if event.mimeData().hasUrls():
|
2025-08-21 22:43:11 +09:00
|
|
|
# Check if any files have audio extensions
|
|
|
|
|
urls = event.mimeData().urls()
|
|
|
|
|
for url in urls:
|
|
|
|
|
file_path = url.toLocalFile()
|
|
|
|
|
if file_path.lower().endswith(('.mp3', '.wav', '.flac')):
|
|
|
|
|
event.accept()
|
|
|
|
|
return
|
|
|
|
|
event.ignore()
|
2024-04-10 20:22:59 +09:00
|
|
|
|
|
|
|
|
def dropEvent(self, event):
|
2025-08-21 22:43:11 +09:00
|
|
|
"""Handle file drop event."""
|
2024-04-10 20:22:59 +09:00
|
|
|
files = [u.toLocalFile() for u in event.mimeData().urls()]
|
2025-08-21 22:43:11 +09:00
|
|
|
audio_files = [f for f in files if f.lower().endswith(('.mp3', '.wav', '.flac'))]
|
|
|
|
|
|
2025-08-21 23:39:53 +09:00
|
|
|
self.logger.info(f"Files dropped: {len(files)} total, {len(audio_files)} audio files")
|
|
|
|
|
|
2025-08-21 22:43:11 +09:00
|
|
|
if audio_files:
|
|
|
|
|
# Analyze the first audio file
|
|
|
|
|
# TODO: Add support for multiple file queue
|
|
|
|
|
file_path = audio_files[0]
|
2025-08-21 23:39:53 +09:00
|
|
|
self.logger.info(f"Starting analysis of dropped file: {os.path.basename(file_path)}")
|
2026-05-30 00:42:45 +09:00
|
|
|
self.analysis_manager.analyze_file(file_path, self.plot_control.current_metric_id())
|
2025-08-21 22:43:11 +09:00
|
|
|
else:
|
|
|
|
|
self.visualization_widget.set_status("No audio files detected in drop")
|
2025-08-21 23:39:53 +09:00
|
|
|
self.logger.warning("No supported audio files found in drop")
|
2025-08-21 22:43:11 +09:00
|
|
|
|
2025-08-22 00:18:31 +09:00
|
|
|
def open_file_dialog(self):
|
|
|
|
|
"""Open file dialog to select audio files for analysis."""
|
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
|
|
|
self,
|
|
|
|
|
"Select Audio File",
|
|
|
|
|
"", # Default directory (empty = current directory)
|
|
|
|
|
"Audio Files (*.mp3 *.wav *.flac);;All Files (*)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if file_path: # User selected a file (didn't cancel)
|
|
|
|
|
self.logger.info(f"File selected via dialog: {os.path.basename(file_path)}")
|
2026-05-30 00:42:45 +09:00
|
|
|
self.analysis_manager.analyze_file(file_path, self.plot_control.current_metric_id())
|
2025-08-22 00:18:31 +09:00
|
|
|
|
2025-08-21 22:43:11 +09:00
|
|
|
def on_analysis_started(self, file_path):
|
|
|
|
|
"""Called when analysis starts."""
|
|
|
|
|
filename = os.path.basename(file_path)
|
|
|
|
|
self.visualization_widget.set_status(f"Analyzing: {filename}...")
|
|
|
|
|
|
|
|
|
|
def on_analysis_completed(self, file_path, result):
|
|
|
|
|
"""Called when analysis completes successfully."""
|
|
|
|
|
filename = os.path.basename(file_path)
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
# Add to file list (checked, so it joins the overlay set) if not present.
|
|
|
|
|
item = self._item_for_path(file_path)
|
|
|
|
|
if item is None:
|
|
|
|
|
self._suppress_list_signals = True
|
2025-08-21 22:43:11 +09:00
|
|
|
item = QListWidgetItem(filename)
|
|
|
|
|
item.setData(Qt.UserRole, file_path) # Store full path
|
2026-06-14 00:35:10 +09:00
|
|
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
|
|
|
item.setCheckState(Qt.Checked)
|
2025-08-21 22:43:11 +09:00
|
|
|
self.file_list.addItem(item)
|
2026-06-14 00:35:10 +09:00
|
|
|
self._suppress_list_signals = False
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
# Update metadata display and highlight the analyzed file.
|
|
|
|
|
self.metadata_display.setText(self.analysis_manager.get_metadata_text(file_path))
|
|
|
|
|
self.file_list.setCurrentItem(item)
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
# Redraw the overlay set for the current metric.
|
|
|
|
|
self._refresh_view()
|
2025-08-21 22:43:11 +09:00
|
|
|
|
|
|
|
|
def on_analysis_error(self, file_path, error_message):
|
|
|
|
|
"""Called when analysis fails."""
|
|
|
|
|
filename = os.path.basename(file_path)
|
2025-08-21 23:39:53 +09:00
|
|
|
self.logger.error(f"Analysis failed for {filename}: {error_message}")
|
2025-08-21 22:43:11 +09:00
|
|
|
self.visualization_widget.set_status(f"Error analyzing {filename}: {error_message}")
|
|
|
|
|
|
2025-08-21 23:39:53 +09:00
|
|
|
def on_progress_update(self, message, percentage):
|
|
|
|
|
"""Called when analysis progress updates."""
|
|
|
|
|
self.logger.debug(f"Progress: {message} ({percentage}%)")
|
|
|
|
|
self.visualization_widget.set_status(f"{message} ({percentage}%)")
|
|
|
|
|
|
2025-08-21 22:43:11 +09:00
|
|
|
def on_file_selected(self, item):
|
2026-06-14 00:35:10 +09:00
|
|
|
"""Called when a file is highlighted (drives the metadata panel only)."""
|
2025-08-21 22:43:11 +09:00
|
|
|
file_path = item.data(Qt.UserRole)
|
2026-06-14 00:35:10 +09:00
|
|
|
self.metadata_display.setText(self.analysis_manager.get_metadata_text(file_path))
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
def on_file_check_changed(self, _item):
|
|
|
|
|
"""A checkbox toggled — the overlay set changed; redraw."""
|
|
|
|
|
if self._suppress_list_signals:
|
|
|
|
|
return
|
|
|
|
|
self._refresh_view()
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2025-08-22 00:18:31 +09:00
|
|
|
def on_font_changed(self, font_name: str, font_type: str):
|
|
|
|
|
"""Called when font selection changes."""
|
|
|
|
|
self.logger.info(f"Font changed via GUI: {font_name} ({font_type})")
|
2026-06-14 00:35:10 +09:00
|
|
|
self._refresh_view()
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2025-08-22 00:18:31 +09:00
|
|
|
def on_font_size_changed(self, font_size: int):
|
|
|
|
|
"""Called when Qt font size changes."""
|
|
|
|
|
self.logger.info(f"Qt font size changed via GUI: {font_size}pt")
|
2026-06-14 00:35:10 +09:00
|
|
|
# Qt font size doesn't affect the plot axes fonts directly; no redraw needed.
|
2026-05-30 00:42:45 +09:00
|
|
|
|
|
|
|
|
def on_metric_changed(self, metric_id: str):
|
|
|
|
|
"""Called when the metric selector changes."""
|
|
|
|
|
self.logger.info(f"Metric changed via GUI: {metric_id}")
|
2026-06-14 00:35:10 +09:00
|
|
|
# The value axis units change with the metric, so custom reference lines
|
|
|
|
|
# placed against the old metric no longer mean anything — drop them.
|
2026-06-14 00:51:33 +09:00
|
|
|
self.ref_lines.clear()
|
|
|
|
|
self._sync_ref_lines()
|
2026-06-14 00:35:10 +09:00
|
|
|
self._refresh_view()
|
|
|
|
|
|
|
|
|
|
def on_add_reference_line(self):
|
2026-06-14 00:51:33 +09:00
|
|
|
"""Add a reference line at the current view centre, then edit it."""
|
|
|
|
|
value = self.visualization_widget.current_view_center_y()
|
|
|
|
|
props = RefLineProps(value=round(value, 2))
|
|
|
|
|
self.ref_lines.append(props)
|
|
|
|
|
self._sync_ref_lines()
|
|
|
|
|
# Open the editor immediately so colour/tag/value can be set right away.
|
|
|
|
|
self.on_edit_reference_line(len(self.ref_lines) - 1)
|
|
|
|
|
|
|
|
|
|
def on_edit_reference_line(self, index: int):
|
|
|
|
|
"""Open the properties dialog for a reference line."""
|
|
|
|
|
if not (0 <= index < len(self.ref_lines)):
|
|
|
|
|
return
|
|
|
|
|
dialog = RefLineDialog(self, self.ref_lines[index])
|
|
|
|
|
if dialog.exec_():
|
|
|
|
|
self.ref_lines[index] = dialog.result_props()
|
|
|
|
|
self._sync_ref_lines()
|
|
|
|
|
|
|
|
|
|
def on_remove_reference_line(self, index: int):
|
|
|
|
|
"""Delete a reference line."""
|
|
|
|
|
if 0 <= index < len(self.ref_lines):
|
|
|
|
|
del self.ref_lines[index]
|
|
|
|
|
self._sync_ref_lines()
|
2026-06-14 00:35:10 +09:00
|
|
|
|
|
|
|
|
def on_clear_reference_lines(self):
|
|
|
|
|
"""Remove all custom reference lines."""
|
2026-06-14 00:51:33 +09:00
|
|
|
self.ref_lines.clear()
|
|
|
|
|
self._sync_ref_lines()
|
|
|
|
|
|
|
|
|
|
def on_reference_line_moved(self, index: int):
|
|
|
|
|
"""A line was dragged on the plot — its value is already updated; refresh list."""
|
|
|
|
|
self.ref_line_control.set_lines(self.ref_lines)
|
|
|
|
|
|
|
|
|
|
def _sync_ref_lines(self):
|
|
|
|
|
"""Push the current reference-line set to both the list view and the plot."""
|
|
|
|
|
self.ref_line_control.set_lines(self.ref_lines)
|
|
|
|
|
self.visualization_widget.set_reference_lines(self.ref_lines)
|
2026-06-14 00:35:10 +09:00
|
|
|
|
|
|
|
|
def on_view_changed(self):
|
|
|
|
|
"""Called when a view-scale toggle (lin/log) changes. Recompute-free redraw."""
|
|
|
|
|
self.logger.info("View scale changed via GUI")
|
|
|
|
|
self._refresh_view()
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2025-08-22 00:18:31 +09:00
|
|
|
def on_plot_refresh_requested(self):
|
|
|
|
|
"""Called when manual plot refresh is requested."""
|
|
|
|
|
self.logger.info("Manual plot refresh requested via GUI")
|
2026-06-14 00:35:10 +09:00
|
|
|
self._refresh_view()
|
2026-05-30 00:42:45 +09:00
|
|
|
|
|
|
|
|
def on_metric_compute_started(self, file_path: str, metric_id: str):
|
|
|
|
|
"""Called when an off-thread metric compute starts."""
|
2026-06-14 00:35:10 +09:00
|
|
|
if file_path not in self._overlay_paths():
|
|
|
|
|
return # not in the drawn set; status bar shouldn't lie
|
2026-05-30 00:42:45 +09:00
|
|
|
metric = METRICS.get(metric_id)
|
|
|
|
|
display = metric.display_name if metric else metric_id
|
|
|
|
|
self.visualization_widget.set_status(f"Computing {display}...")
|
|
|
|
|
|
|
|
|
|
def on_metric_ready(self, file_path: str, metric_id: str):
|
|
|
|
|
"""Called when metric data is available (cached hit or async finish)."""
|
|
|
|
|
if metric_id != self.plot_control.current_metric_id():
|
|
|
|
|
return # user already switched to a different metric
|
2026-06-14 00:35:10 +09:00
|
|
|
if file_path not in self._overlay_paths():
|
|
|
|
|
return # no longer part of the overlay set
|
|
|
|
|
self._refresh_view()
|
2026-05-30 00:42:45 +09:00
|
|
|
|
|
|
|
|
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}")
|
2026-06-14 00:35:10 +09:00
|
|
|
if file_path in self._overlay_paths():
|
2026-05-30 00:42:45 +09:00
|
|
|
self.visualization_widget.set_status(f"Error computing {metric_id}: {error_message}")
|
|
|
|
|
|
|
|
|
|
def _current_file_path(self):
|
|
|
|
|
item = self.file_list.currentItem()
|
|
|
|
|
return item.data(Qt.UserRole) if item else None
|
|
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
def _item_for_path(self, file_path):
|
|
|
|
|
for i in range(self.file_list.count()):
|
|
|
|
|
item = self.file_list.item(i)
|
|
|
|
|
if item.data(Qt.UserRole) == file_path:
|
|
|
|
|
return item
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _row_index(self, file_path) -> int:
|
|
|
|
|
for i in range(self.file_list.count()):
|
|
|
|
|
if self.file_list.item(i).data(Qt.UserRole) == file_path:
|
|
|
|
|
return i
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
def _overlay_paths(self):
|
|
|
|
|
"""File paths whose checkbox is ticked — the set drawn on the graph."""
|
|
|
|
|
return [
|
|
|
|
|
self.file_list.item(i).data(Qt.UserRole)
|
|
|
|
|
for i in range(self.file_list.count())
|
|
|
|
|
if self.file_list.item(i).checkState() == Qt.Checked
|
|
|
|
|
]
|
2026-05-30 00:42:45 +09:00
|
|
|
|
2026-06-14 00:35:10 +09:00
|
|
|
def _refresh_view(self):
|
|
|
|
|
"""Redraw the checked overlay set for the current metric and view-state.
|
|
|
|
|
|
|
|
|
|
Renders every dataset whose data is cached; for any that isn't, kicks off
|
|
|
|
|
an async compute (or a full load if the file was never analysed) and
|
|
|
|
|
leaves a status note. `on_metric_ready` calls back here when each lands.
|
2026-05-30 00:42:45 +09:00
|
|
|
"""
|
2026-06-14 00:35:10 +09:00
|
|
|
paths = self._overlay_paths()
|
2026-05-30 00:42:45 +09:00
|
|
|
metric_id = self.plot_control.current_metric_id()
|
2026-06-14 00:35:10 +09:00
|
|
|
view = self.plot_control.current_view_state()
|
|
|
|
|
metric = METRICS.get(metric_id)
|
|
|
|
|
if not paths or metric is None:
|
|
|
|
|
self.visualization_widget.show_specs([])
|
2026-05-30 00:42:45 +09:00
|
|
|
return
|
2026-06-14 00:35:10 +09:00
|
|
|
|
|
|
|
|
specs = []
|
|
|
|
|
pending = 0
|
|
|
|
|
for path in paths:
|
|
|
|
|
data = self.analysis_manager.get_metric_data(path, metric_id)
|
|
|
|
|
if data is None:
|
|
|
|
|
if self.analysis_manager.is_file_analyzed(path):
|
|
|
|
|
self.analysis_manager.request_metric(path, metric_id)
|
|
|
|
|
else:
|
|
|
|
|
self.analysis_manager.analyze_file(path, metric_id)
|
|
|
|
|
pending += 1
|
|
|
|
|
continue
|
|
|
|
|
label = self.analysis_manager.display_label(path)
|
|
|
|
|
# Colour is keyed to the file's row, not its position in the overlay
|
|
|
|
|
# subset, so a song keeps its colour as others are ticked/unticked.
|
|
|
|
|
color = dataset_color(self._row_index(path))
|
2026-06-14 00:51:33 +09:00
|
|
|
spec = apply_x_mode(metric.build_spec(data, view), view.x_mode)
|
|
|
|
|
specs.append((label, spec, color))
|
2026-06-14 00:35:10 +09:00
|
|
|
|
|
|
|
|
if specs:
|
|
|
|
|
self.visualization_widget.show_specs(specs, view)
|
|
|
|
|
if pending:
|
|
|
|
|
self.visualization_widget.set_status(
|
|
|
|
|
f"Computing {metric.display_name} for {pending} file(s)..."
|
|
|
|
|
)
|
2025-08-21 22:43:11 +09:00
|
|
|
|
2024-04-10 20:22:59 +09:00
|
|
|
|
2026-05-28 14:05:34 +09:00
|
|
|
def main():
|
2025-08-21 23:39:53 +09:00
|
|
|
# Parse logging arguments before creating QApplication
|
|
|
|
|
log_level, log_to_file = parse_log_args()
|
2026-05-28 14:05:34 +09:00
|
|
|
|
2025-08-21 23:39:53 +09:00
|
|
|
# Initialize logging
|
|
|
|
|
logger = setup_logging(log_level, log_to_file)
|
|
|
|
|
logger.info("Starting Audio Mastering Analysis Toolkit")
|
|
|
|
|
logger.info(f"Command line args: log-level={log_level}, log-file={log_to_file}")
|
2026-05-28 14:05:34 +09:00
|
|
|
|
2024-04-10 20:22:59 +09:00
|
|
|
app = QApplication(sys.argv)
|
2026-05-28 14:05:34 +09:00
|
|
|
|
2025-08-21 23:39:53 +09:00
|
|
|
# Initialize font system before creating any widgets
|
|
|
|
|
font_success = initialize_fonts()
|
|
|
|
|
if font_success:
|
|
|
|
|
logger.info("Font system initialized successfully")
|
|
|
|
|
# Log font status for debugging
|
|
|
|
|
font_status = get_font_manager().get_status_report()
|
|
|
|
|
logger.debug(f"Font status: matplotlib={font_status['matplotlib_configured']}, "
|
|
|
|
|
f"qt={font_status['qt_configured']}, "
|
|
|
|
|
f"custom_fonts={font_status['custom_fonts_loaded']}")
|
|
|
|
|
else:
|
|
|
|
|
logger.warning("Font system initialization failed - CJK characters may not display properly")
|
2026-05-28 14:05:34 +09:00
|
|
|
|
2025-08-21 22:43:11 +09:00
|
|
|
# Set application style
|
|
|
|
|
app.setStyle('Fusion') # Modern cross-platform style
|
2025-08-21 23:39:53 +09:00
|
|
|
logger.debug("Application style set to Fusion")
|
2026-05-28 14:05:34 +09:00
|
|
|
|
2025-08-21 22:43:11 +09:00
|
|
|
window = MainWindow()
|
|
|
|
|
window.show()
|
2025-08-21 23:39:53 +09:00
|
|
|
logger.info("GUI window displayed")
|
2026-05-28 14:05:34 +09:00
|
|
|
|
|
|
|
|
sys.exit(app.exec_())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|