183 lines
6.7 KiB
Python
183 lines
6.7 KiB
Python
"""
|
|
Analysis Results Manager - Bridge between audio processing and GUI.
|
|
Manages analysis queue and coordinates between components.
|
|
"""
|
|
|
|
from PyQt5.QtCore import QObject, pyqtSignal, QThread
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
import os
|
|
import logging
|
|
|
|
from master_core import AudioFile
|
|
from plotting_engine import PlottingEngine
|
|
|
|
|
|
@dataclass
|
|
class AnalysisResult:
|
|
"""Container for audio analysis results."""
|
|
file_path: str
|
|
song_name: str
|
|
bpm: float
|
|
max_amplitude: float
|
|
avg_amplitude: float
|
|
times: list
|
|
rms_array: list
|
|
analysis_successful: bool = True
|
|
error_message: str = ""
|
|
|
|
|
|
class AudioAnalysisWorker(QThread):
|
|
"""
|
|
Worker thread for audio analysis to prevent GUI freezing.
|
|
Performs heavy librosa operations in background.
|
|
"""
|
|
|
|
# Signals for communicating with main thread
|
|
progressUpdate = pyqtSignal(str, int) # message, percentage
|
|
analysisCompleted = pyqtSignal(str, object) # file_path, AnalysisResult
|
|
analysisError = pyqtSignal(str, str) # file_path, error_message
|
|
|
|
def __init__(self, file_path: str, window: int = 10, hop: int = 2):
|
|
super().__init__()
|
|
self.file_path = file_path
|
|
self.window = window
|
|
self.hop = hop
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
def run(self):
|
|
"""Main thread execution - performs audio analysis."""
|
|
try:
|
|
self.logger.info(f"Starting analysis of: {os.path.basename(self.file_path)}")
|
|
self.progressUpdate.emit("Loading audio file...", 10)
|
|
|
|
# Create AudioFile and load audio data
|
|
audio_file = AudioFile(self.file_path)
|
|
self.progressUpdate.emit("Audio loaded, detecting tempo...", 30)
|
|
|
|
# BPM is already calculated in __init__, now do RMS analysis
|
|
self.progressUpdate.emit("Computing RMS power levels...", 60)
|
|
audio_file.get_energy_levels_over_time(window=self.window, hop=self.hop)
|
|
|
|
self.progressUpdate.emit("Finalizing analysis...", 90)
|
|
|
|
# Extract analysis results
|
|
result = AnalysisResult(
|
|
file_path=self.file_path,
|
|
song_name=audio_file.song_name,
|
|
bpm=audio_file.get_bpm(),
|
|
max_amplitude=audio_file.max_amplitude,
|
|
avg_amplitude=audio_file.avg_amplitude,
|
|
times=audio_file.get_times(),
|
|
rms_array=audio_file.rms_array,
|
|
analysis_successful=True
|
|
)
|
|
|
|
self.progressUpdate.emit("Analysis complete!", 100)
|
|
self.logger.info(f"Analysis completed: {os.path.basename(self.file_path)} (BPM: {result.bpm:.1f})")
|
|
|
|
# Emit success signal
|
|
self.analysisCompleted.emit(self.file_path, result)
|
|
|
|
except Exception as e:
|
|
error_msg = f"Analysis failed: {str(e)}"
|
|
self.logger.error(f"Analysis error for {self.file_path}: {error_msg}")
|
|
self.analysisError.emit(self.file_path, error_msg)
|
|
|
|
|
|
class AnalysisResultsManager(QObject):
|
|
"""
|
|
Manages audio file analysis and coordinates between processing and GUI.
|
|
Now uses background threads to prevent GUI freezing.
|
|
"""
|
|
|
|
# Signals for GUI communication
|
|
analysisStarted = pyqtSignal(str) # file_path
|
|
analysisCompleted = pyqtSignal(str, object) # file_path, AnalysisResult
|
|
analysisError = pyqtSignal(str, str) # file_path, error_message
|
|
progressUpdate = pyqtSignal(str, int) # message, percentage
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.results_cache = {} # Store analysis results
|
|
self.plotting_engine = PlottingEngine()
|
|
self.current_worker = None # Track active worker thread
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
def analyze_file(self, file_path: str, window: int = 10, hop: int = 2):
|
|
"""
|
|
Analyze an audio file using background thread to prevent GUI freezing.
|
|
|
|
Args:
|
|
file_path: Path to audio file
|
|
window: RMS analysis window size in seconds
|
|
hop: Analysis hop size in seconds
|
|
"""
|
|
if not os.path.exists(file_path):
|
|
error_msg = f"File not found: {file_path}"
|
|
self.logger.error(error_msg)
|
|
self.analysisError.emit(file_path, error_msg)
|
|
return
|
|
|
|
# Stop any existing worker
|
|
if self.current_worker and self.current_worker.isRunning():
|
|
self.logger.info("Stopping previous analysis to start new one")
|
|
self.current_worker.quit()
|
|
self.current_worker.wait()
|
|
|
|
# Emit analysis started signal
|
|
self.analysisStarted.emit(file_path)
|
|
self.logger.info(f"Queuing analysis: {os.path.basename(file_path)}")
|
|
|
|
# Create and start worker thread
|
|
self.current_worker = AudioAnalysisWorker(file_path, window, hop)
|
|
|
|
# Connect worker signals
|
|
self.current_worker.progressUpdate.connect(self.progressUpdate.emit)
|
|
self.current_worker.analysisCompleted.connect(self._on_worker_completed)
|
|
self.current_worker.analysisError.connect(self.analysisError.emit)
|
|
|
|
# Start the background analysis
|
|
self.current_worker.start()
|
|
|
|
def _on_worker_completed(self, file_path: str, result: AnalysisResult):
|
|
"""Handle completion of worker thread analysis."""
|
|
# Cache the result
|
|
self.results_cache[file_path] = result
|
|
|
|
# Forward the signal to GUI
|
|
self.analysisCompleted.emit(file_path, result)
|
|
|
|
def get_analysis_figure(self, file_path: str):
|
|
"""
|
|
Get matplotlib figure for a previously analyzed file.
|
|
|
|
Returns:
|
|
matplotlib.figure.Figure or None
|
|
"""
|
|
if file_path not in self.results_cache:
|
|
return None
|
|
|
|
result = self.results_cache[file_path]
|
|
return self.plotting_engine.create_power_analysis_figure(
|
|
result.times, result.rms_array, result.file_path
|
|
)
|
|
|
|
def get_metadata_text(self, file_path: str) -> str:
|
|
"""Get formatted metadata text for a file."""
|
|
if file_path not in self.results_cache:
|
|
return "No analysis data available"
|
|
|
|
result = self.results_cache[file_path]
|
|
return self.plotting_engine.create_metadata_display_text(
|
|
result.song_name, result.bpm,
|
|
result.max_amplitude, result.avg_amplitude
|
|
)
|
|
|
|
def clear_cache(self):
|
|
"""Clear all cached analysis results."""
|
|
self.results_cache.clear()
|
|
|
|
def is_file_analyzed(self, file_path: str) -> bool:
|
|
"""Check if a file has been analyzed."""
|
|
return file_path in self.results_cache |