Implement threading, logging, and CJK font support
Major improvements to GUI stability and internationalization: - Fix GUI freezing by implementing threaded audio analysis - Add AudioAnalysisWorker thread for background processing - Progress signals with percentage updates - Thread-safe communication via Qt signals - Add comprehensive CLI logging system - 5 log levels: ERROR, WARN, INFO, DEBUG, TRACE - Command line control: --log-level, --log-file - Real-time feedback during analysis operations - Implement CJK font fallback system - FontManager with 3-tier fallback (custom → system → default) - Cross-platform CJK font detection (Windows/macOS/Linux) - Licensing-safe fonts/ directory with gitignored font files - Setup utility and comprehensive documentation - Fix numpy array formatting issue with BPM detection - Add progress indicators for long-running operations - Preserve fonts directory structure with placeholder file 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+91
-32
@@ -3,10 +3,11 @@ Analysis Results Manager - Bridge between audio processing and GUI.
|
||||
Manages analysis queue and coordinates between components.
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
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
|
||||
@@ -26,26 +27,86 @@ class AnalysisResult:
|
||||
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.
|
||||
Threading-ready architecture for future background processing.
|
||||
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 and emit results.
|
||||
Currently synchronous - ready for threading later.
|
||||
Analyze an audio file using background thread to prevent GUI freezing.
|
||||
|
||||
Args:
|
||||
file_path: Path to audio file
|
||||
@@ -54,40 +115,38 @@ class AnalysisResultsManager(QObject):
|
||||
"""
|
||||
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)}")
|
||||
|
||||
try:
|
||||
# Create AudioFile and perform analysis
|
||||
audio_file = AudioFile(file_path)
|
||||
|
||||
# Get RMS analysis data
|
||||
audio_file.get_energy_levels_over_time(window=window, hop=hop)
|
||||
|
||||
# Extract analysis results
|
||||
result = AnalysisResult(
|
||||
file_path=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(), # We'll need to add this method
|
||||
rms_array=audio_file.rms_array,
|
||||
analysis_successful=True
|
||||
)
|
||||
|
||||
# Cache the result
|
||||
self.results_cache[file_path] = result
|
||||
|
||||
# Emit completion signal
|
||||
self.analysisCompleted.emit(file_path, result)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Analysis failed: {str(e)}"
|
||||
self.analysisError.emit(file_path, error_msg)
|
||||
# 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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user