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:
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebFetch(domain:docs.anthropic.com)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(lsb_release:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"WebFetch(domain:docs.github.com)",
|
||||||
|
"WebFetch(domain:docs.github.com)",
|
||||||
|
"Bash(ssh:*)",
|
||||||
|
"Bash(rm:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": [],
|
||||||
|
"defaultMode": "acceptEdits"
|
||||||
|
},
|
||||||
|
"outputStyle": "haruki"
|
||||||
|
}
|
||||||
+23
@@ -1 +1,24 @@
|
|||||||
files.txt
|
files.txt
|
||||||
|
|
||||||
|
# Font directory - avoid licensing issues by not committing font files
|
||||||
|
# Keep the directory structure but ignore actual font files
|
||||||
|
fonts/*.ttf
|
||||||
|
fonts/*.otf
|
||||||
|
fonts/*.ttc
|
||||||
|
# But preserve the placeholder file
|
||||||
|
!fonts/PLACE_YOUR_FONT_FILES_HERE
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
# CJK Font Support Implementation
|
||||||
|
|
||||||
|
This document describes the CJK (Chinese, Japanese, Korean) font fallback system implemented for the Audio Analysis Toolkit.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The application displays song titles and metadata that may contain CJK characters from Japanese, Chinese, or Korean music files. The default matplotlib font (DejaVu Sans) lacks CJK glyphs, causing:
|
||||||
|
- Matplotlib warnings about missing glyphs
|
||||||
|
- Incorrect character rendering (squares, question marks, etc.)
|
||||||
|
- Poor user experience for CJK music collections
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
### 1. Font Manager System (`font_manager.py`)
|
||||||
|
|
||||||
|
A centralized font management system that handles both matplotlib and Qt font configuration:
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- **Licensing-safe**: Uses local `fonts/` directory (gitignored) for custom fonts
|
||||||
|
- **Graceful fallback**: System CJK fonts → matplotlib defaults
|
||||||
|
- **Cross-platform**: Windows, macOS, Linux font detection
|
||||||
|
- **Modular design**: Single responsibility for font configuration
|
||||||
|
|
||||||
|
**Font Priority Order:**
|
||||||
|
1. Custom fonts from `fonts/` directory (highest priority)
|
||||||
|
2. System CJK fonts (platform-specific)
|
||||||
|
3. Default fonts (fallback)
|
||||||
|
|
||||||
|
### 2. Safe Title Processing
|
||||||
|
|
||||||
|
All text that might contain CJK characters is processed through `safe_title()` function:
|
||||||
|
- Ensures proper encoding handling
|
||||||
|
- Provides fallback for problematic characters
|
||||||
|
- Maintains original text when possible
|
||||||
|
|
||||||
|
### 3. Integration Points
|
||||||
|
|
||||||
|
The font system is integrated at these key locations:
|
||||||
|
|
||||||
|
#### Application Startup (`main.py`)
|
||||||
|
```python
|
||||||
|
# Initialize font system before creating any widgets
|
||||||
|
font_success = initialize_fonts()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Plot Titles (`plotting_engine.py`, `master_core.py`)
|
||||||
|
```python
|
||||||
|
ax.set_title(safe_title(os.path.basename(file_path)))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Metadata Display
|
||||||
|
```python
|
||||||
|
song_name = safe_title(f"{audio['artist'][0]} - {audio['title'][0]}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Instructions
|
||||||
|
|
||||||
|
### Quick Setup
|
||||||
|
|
||||||
|
1. **Run the setup utility:**
|
||||||
|
```bash
|
||||||
|
python setup_fonts.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **For enhanced CJK support, add fonts to the `fonts/` directory:**
|
||||||
|
- Download free CJK fonts (Noto Sans CJK, Source Han Sans, etc.)
|
||||||
|
- Place .ttf/.otf/.ttc files in `fonts/` directory
|
||||||
|
- Restart the application
|
||||||
|
|
||||||
|
### Font Directory Structure
|
||||||
|
```
|
||||||
|
uj-mastering-master/
|
||||||
|
├── fonts/ # Gitignored
|
||||||
|
│ ├── NotoSansCJK-Regular.ttc
|
||||||
|
│ ├── SourceHanSans-Regular.otf
|
||||||
|
│ └── [other CJK fonts]
|
||||||
|
└── [application files]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Font Formats
|
||||||
|
- `.ttf` (TrueType Font)
|
||||||
|
- `.otf` (OpenType Font)
|
||||||
|
- `.ttc` (TrueType Collection)
|
||||||
|
|
||||||
|
## Platform-Specific Behavior
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
**System Fonts Used:**
|
||||||
|
- Yu Gothic UI, Meiryo, MS Gothic (sans-serif)
|
||||||
|
- Yu Mincho, MS Mincho (serif)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
**System Fonts Used:**
|
||||||
|
- Hiragino Sans, Yu Gothic (sans-serif)
|
||||||
|
- Hiragino Mincho ProN, Yu Mincho (serif)
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
**System Fonts Used:**
|
||||||
|
- Noto Sans CJK JP, Source Han Sans (sans-serif)
|
||||||
|
- Noto Serif CJK JP, Source Han Serif (serif)
|
||||||
|
|
||||||
|
## Technical Implementation Details
|
||||||
|
|
||||||
|
### Font Detection Algorithm
|
||||||
|
|
||||||
|
1. **Custom Font Loading:**
|
||||||
|
```python
|
||||||
|
# Load for matplotlib
|
||||||
|
fm.fontManager.addfont(str(font_file))
|
||||||
|
|
||||||
|
# Load for Qt
|
||||||
|
font_id = QFontDatabase.addApplicationFont(str(font_file))
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **System Font Fallback:**
|
||||||
|
```python
|
||||||
|
available_fonts = set(fm.get_font_names())
|
||||||
|
for font_name in system_fonts['sans-serif']:
|
||||||
|
if font_name in available_fonts:
|
||||||
|
return font_name
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Matplotlib Configuration:**
|
||||||
|
```python
|
||||||
|
plt.rcParams['font.sans-serif'] = font_list
|
||||||
|
plt.rcParams['font.family'] = 'sans-serif'
|
||||||
|
plt.rcParams['axes.unicode_minus'] = False
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
The system is designed to be fault-tolerant:
|
||||||
|
- Missing fonts directory → Use system fonts
|
||||||
|
- Font loading failures → Log warnings, continue
|
||||||
|
- Encoding errors → Apply safe character replacement
|
||||||
|
- No CJK fonts found → Graceful degradation to defaults
|
||||||
|
|
||||||
|
## Licensing Considerations
|
||||||
|
|
||||||
|
### Safe Practices
|
||||||
|
- **Custom fonts directory is gitignored** to avoid committing proprietary fonts
|
||||||
|
- **System fonts are detected, not redistributed**
|
||||||
|
- **Open source font recommendations** (Noto, Source Han families)
|
||||||
|
- **No font files included in repository**
|
||||||
|
|
||||||
|
### Recommended Free CJK Fonts
|
||||||
|
1. **Google Noto Fonts** (SIL Open Font License)
|
||||||
|
- Noto Sans CJK JP/SC/TC/KR
|
||||||
|
- Comprehensive CJK coverage
|
||||||
|
|
||||||
|
2. **Adobe Source Han Fonts** (SIL Open Font License)
|
||||||
|
- Source Han Sans
|
||||||
|
- Source Han Serif
|
||||||
|
|
||||||
|
## Testing and Debugging
|
||||||
|
|
||||||
|
### Font Status Report
|
||||||
|
```python
|
||||||
|
from font_manager import get_font_manager
|
||||||
|
status = get_font_manager().get_status_report()
|
||||||
|
print(status)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test CJK Characters
|
||||||
|
```bash
|
||||||
|
python setup_fonts.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
Font system operations are logged at appropriate levels:
|
||||||
|
- INFO: Successful initialization
|
||||||
|
- DEBUG: Font loading details
|
||||||
|
- WARNING: Missing fonts, fallbacks used
|
||||||
|
- ERROR: Critical font system failures
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
### Potential Enhancements
|
||||||
|
1. **Dynamic Font Switching:** Per-language font selection
|
||||||
|
2. **Font Caching:** Faster startup with font cache
|
||||||
|
3. **User Preferences:** GUI for font selection
|
||||||
|
4. **Font Metrics:** Analyze font quality for CJK rendering
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
- Font loading is done once at startup
|
||||||
|
- Font cache clearing only when necessary
|
||||||
|
- Minimal performance impact on audio processing
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Issue:** CJK characters still show as squares
|
||||||
|
- **Solution:** Install CJK fonts in `fonts/` directory or check system font availability
|
||||||
|
|
||||||
|
**Issue:** Font warnings in console
|
||||||
|
- **Solution:** Run `python setup_fonts.py` to check font configuration
|
||||||
|
|
||||||
|
**Issue:** Application startup slower after font system
|
||||||
|
- **Solution:** This is normal on first run; subsequent starts should be faster
|
||||||
|
|
||||||
|
### Debug Commands
|
||||||
|
```bash
|
||||||
|
# Check font system status
|
||||||
|
python setup_fonts.py
|
||||||
|
|
||||||
|
# Test with specific log level
|
||||||
|
python main.py --log-level DEBUG
|
||||||
|
|
||||||
|
# Test matplotlib font configuration
|
||||||
|
python -c "import matplotlib.pyplot as plt; print(plt.rcParams['font.sans-serif'])"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This CJK font support implementation provides:
|
||||||
|
- **Robust fallback system** ensuring CJK characters display properly
|
||||||
|
- **Licensing compliance** by avoiding font redistribution
|
||||||
|
- **Cross-platform compatibility** with platform-specific font preferences
|
||||||
|
- **Clean architecture** with separation of font management concerns
|
||||||
|
- **User-friendly setup** with clear instructions and status reporting
|
||||||
|
|
||||||
|
The system gracefully handles missing fonts and provides clear guidance for optimal CJK character rendering while maintaining the existing application functionality.
|
||||||
+87
-28
@@ -3,10 +3,11 @@ Analysis Results Manager - Bridge between audio processing and GUI.
|
|||||||
Manages analysis queue and coordinates between components.
|
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 dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
from master_core import AudioFile
|
from master_core import AudioFile
|
||||||
from plotting_engine import PlottingEngine
|
from plotting_engine import PlottingEngine
|
||||||
@@ -26,26 +27,86 @@ class AnalysisResult:
|
|||||||
error_message: str = ""
|
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):
|
class AnalysisResultsManager(QObject):
|
||||||
"""
|
"""
|
||||||
Manages audio file analysis and coordinates between processing and GUI.
|
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
|
# Signals for GUI communication
|
||||||
analysisStarted = pyqtSignal(str) # file_path
|
analysisStarted = pyqtSignal(str) # file_path
|
||||||
analysisCompleted = pyqtSignal(str, object) # file_path, AnalysisResult
|
analysisCompleted = pyqtSignal(str, object) # file_path, AnalysisResult
|
||||||
analysisError = pyqtSignal(str, str) # file_path, error_message
|
analysisError = pyqtSignal(str, str) # file_path, error_message
|
||||||
|
progressUpdate = pyqtSignal(str, int) # message, percentage
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.results_cache = {} # Store analysis results
|
self.results_cache = {} # Store analysis results
|
||||||
self.plotting_engine = PlottingEngine()
|
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):
|
def analyze_file(self, file_path: str, window: int = 10, hop: int = 2):
|
||||||
"""
|
"""
|
||||||
Analyze an audio file and emit results.
|
Analyze an audio file using background thread to prevent GUI freezing.
|
||||||
Currently synchronous - ready for threading later.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: Path to audio file
|
file_path: Path to audio file
|
||||||
@@ -54,40 +115,38 @@ class AnalysisResultsManager(QObject):
|
|||||||
"""
|
"""
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
error_msg = f"File not found: {file_path}"
|
error_msg = f"File not found: {file_path}"
|
||||||
|
self.logger.error(error_msg)
|
||||||
self.analysisError.emit(file_path, error_msg)
|
self.analysisError.emit(file_path, error_msg)
|
||||||
return
|
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
|
# Emit analysis started signal
|
||||||
self.analysisStarted.emit(file_path)
|
self.analysisStarted.emit(file_path)
|
||||||
|
self.logger.info(f"Queuing analysis: {os.path.basename(file_path)}")
|
||||||
|
|
||||||
try:
|
# Create and start worker thread
|
||||||
# Create AudioFile and perform analysis
|
self.current_worker = AudioAnalysisWorker(file_path, window, hop)
|
||||||
audio_file = AudioFile(file_path)
|
|
||||||
|
|
||||||
# Get RMS analysis data
|
# Connect worker signals
|
||||||
audio_file.get_energy_levels_over_time(window=window, hop=hop)
|
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)
|
||||||
|
|
||||||
# Extract analysis results
|
# Start the background analysis
|
||||||
result = AnalysisResult(
|
self.current_worker.start()
|
||||||
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
|
def _on_worker_completed(self, file_path: str, result: AnalysisResult):
|
||||||
self.results_cache[file_path] = result
|
"""Handle completion of worker thread analysis."""
|
||||||
|
# Cache the result
|
||||||
|
self.results_cache[file_path] = result
|
||||||
|
|
||||||
# Emit completion signal
|
# Forward the signal to GUI
|
||||||
self.analysisCompleted.emit(file_path, result)
|
self.analysisCompleted.emit(file_path, result)
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Analysis failed: {str(e)}"
|
|
||||||
self.analysisError.emit(file_path, error_msg)
|
|
||||||
|
|
||||||
def get_analysis_figure(self, file_path: str):
|
def get_analysis_figure(self, file_path: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Demo script showing the different logging levels available.
|
||||||
|
Usage examples:
|
||||||
|
|
||||||
|
python demo_logging.py --log-level=ERROR
|
||||||
|
python demo_logging.py --log-level=INFO
|
||||||
|
python demo_logging.py --log-level=DEBUG
|
||||||
|
python demo_logging.py --log-level=TRACE --log-file
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from logger_setup import setup_logging, parse_log_args
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Parse logging configuration
|
||||||
|
log_level, log_to_file = parse_log_args()
|
||||||
|
|
||||||
|
# Initialize logging
|
||||||
|
logger = setup_logging(log_level, log_to_file)
|
||||||
|
|
||||||
|
# Demo different log levels
|
||||||
|
print(f"\n=== Audio Mastering Toolkit Logging Demo ===")
|
||||||
|
print(f"Log Level: {log_level}")
|
||||||
|
print(f"Log to File: {log_to_file}")
|
||||||
|
print(f"============================================\n")
|
||||||
|
|
||||||
|
# Test all logging levels
|
||||||
|
logger.error("This is an ERROR message - critical failures only")
|
||||||
|
logger.warning("This is a WARNING message - non-fatal issues")
|
||||||
|
logger.info("This is an INFO message - key operations")
|
||||||
|
logger.debug("This is a DEBUG message - detailed processing steps")
|
||||||
|
logger.trace("This is a TRACE message - granular details")
|
||||||
|
|
||||||
|
print(f"\nDemo complete! Messages above {log_level} level are visible.")
|
||||||
|
if log_to_file:
|
||||||
|
print("Check 'audio_analysis.log' for file output.")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
+357
@@ -0,0 +1,357 @@
|
|||||||
|
"""
|
||||||
|
Font management system with CJK fallback support.
|
||||||
|
Handles matplotlib and Qt font configuration with licensing-safe approach.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import platform
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.font_manager as fm
|
||||||
|
from PyQt5.QtCore import QCoreApplication
|
||||||
|
from PyQt5.QtGui import QFontDatabase, QFont
|
||||||
|
|
||||||
|
|
||||||
|
class FontManager:
|
||||||
|
"""
|
||||||
|
Centralized font management for CJK character support.
|
||||||
|
|
||||||
|
Provides licensing-safe font fallback by:
|
||||||
|
1. Loading fonts from local fonts/ directory (gitignored)
|
||||||
|
2. Falling back to system CJK fonts
|
||||||
|
3. Gracefully degrading to default fonts
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fonts_dir: str = "fonts"):
|
||||||
|
"""
|
||||||
|
Initialize font manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fonts_dir: Directory name for custom fonts (relative to project root)
|
||||||
|
"""
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.fonts_dir = Path(__file__).parent / fonts_dir
|
||||||
|
self.loaded_fonts: Dict[str, str] = {}
|
||||||
|
self._matplotlib_configured = False
|
||||||
|
self._qt_configured = False
|
||||||
|
|
||||||
|
# CJK font preferences by platform and type
|
||||||
|
self.system_font_fallbacks = {
|
||||||
|
'Windows': {
|
||||||
|
'serif': ['Yu Mincho', 'MS Mincho', '游明朝', 'MS 明朝'],
|
||||||
|
'sans-serif': ['Yu Gothic UI', 'Meiryo', 'MS Gothic', '游ゴシック', 'メイリオ', 'MS ゴシック'],
|
||||||
|
'monospace': ['MS Gothic', 'MS ゴシック']
|
||||||
|
},
|
||||||
|
'Darwin': { # macOS
|
||||||
|
'serif': ['Hiragino Mincho ProN', 'Yu Mincho', 'Times New Roman'],
|
||||||
|
'sans-serif': ['Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Arial Unicode MS'],
|
||||||
|
'monospace': ['Menlo', 'Monaco', 'Courier New']
|
||||||
|
},
|
||||||
|
'Linux': {
|
||||||
|
'serif': ['Noto Serif CJK JP', 'Source Han Serif', 'DejaVu Serif'],
|
||||||
|
'sans-serif': ['Noto Sans CJK JP', 'Source Han Sans', 'DejaVu Sans'],
|
||||||
|
'monospace': ['Noto Sans Mono CJK JP', 'Source Code Pro', 'DejaVu Sans Mono']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""
|
||||||
|
Initialize font system for both matplotlib and Qt.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if initialization was successful
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.logger.info("Initializing font management system...")
|
||||||
|
|
||||||
|
# Load custom fonts if available
|
||||||
|
custom_fonts_loaded = self._load_custom_fonts()
|
||||||
|
|
||||||
|
# Configure matplotlib
|
||||||
|
matplotlib_success = self._configure_matplotlib()
|
||||||
|
|
||||||
|
# Configure Qt
|
||||||
|
qt_success = self._configure_qt()
|
||||||
|
|
||||||
|
success = matplotlib_success and qt_success
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.logger.info(f"Font system initialized successfully. Custom fonts: {custom_fonts_loaded}")
|
||||||
|
else:
|
||||||
|
self.logger.warning("Font system initialized with some issues")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Font system initialization failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _load_custom_fonts(self) -> int:
|
||||||
|
"""
|
||||||
|
Load fonts from the fonts/ directory if it exists.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Number of custom fonts loaded
|
||||||
|
"""
|
||||||
|
if not self.fonts_dir.exists():
|
||||||
|
self.logger.info(f"Custom fonts directory {self.fonts_dir} not found - using system fonts")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
font_extensions = {'.ttf', '.otf', '.ttc'}
|
||||||
|
fonts_loaded = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
for font_file in self.fonts_dir.iterdir():
|
||||||
|
if font_file.suffix.lower() in font_extensions:
|
||||||
|
try:
|
||||||
|
# Load for matplotlib
|
||||||
|
fm.fontManager.addfont(str(font_file))
|
||||||
|
|
||||||
|
# Load for Qt
|
||||||
|
font_id = QFontDatabase.addApplicationFont(str(font_file))
|
||||||
|
if font_id != -1:
|
||||||
|
font_families = QFontDatabase.applicationFontFamilies(font_id)
|
||||||
|
for family in font_families:
|
||||||
|
self.loaded_fonts[family] = str(font_file)
|
||||||
|
|
||||||
|
fonts_loaded += 1
|
||||||
|
self.logger.debug(f"Loaded custom font: {font_file.name} -> {font_families}")
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Failed to load font for Qt: {font_file.name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Failed to load custom font {font_file.name}: {e}")
|
||||||
|
|
||||||
|
if fonts_loaded > 0:
|
||||||
|
# Clear matplotlib's font cache to recognize new fonts
|
||||||
|
fm.fontManager._load_fontmanager(try_read_cache=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error loading custom fonts: {e}")
|
||||||
|
|
||||||
|
return fonts_loaded
|
||||||
|
|
||||||
|
def _configure_matplotlib(self) -> bool:
|
||||||
|
"""Configure matplotlib with appropriate CJK font fallbacks."""
|
||||||
|
try:
|
||||||
|
current_system = platform.system()
|
||||||
|
fallback_fonts = self.system_font_fallbacks.get(current_system,
|
||||||
|
self.system_font_fallbacks['Linux'])
|
||||||
|
|
||||||
|
# Build font list: custom fonts + system fallbacks + matplotlib defaults
|
||||||
|
font_list = []
|
||||||
|
|
||||||
|
# Add custom fonts first (highest priority)
|
||||||
|
font_list.extend(self.loaded_fonts.keys())
|
||||||
|
|
||||||
|
# Add system CJK fonts
|
||||||
|
font_list.extend(fallback_fonts['sans-serif'])
|
||||||
|
|
||||||
|
# Add matplotlib defaults as final fallback
|
||||||
|
font_list.extend(['DejaVu Sans', 'Arial', 'sans-serif'])
|
||||||
|
|
||||||
|
# Update matplotlib configuration
|
||||||
|
plt.rcParams['font.sans-serif'] = font_list
|
||||||
|
plt.rcParams['font.family'] = 'sans-serif'
|
||||||
|
|
||||||
|
# Ensure matplotlib can handle Unicode
|
||||||
|
plt.rcParams['axes.unicode_minus'] = False
|
||||||
|
|
||||||
|
self.logger.info(f"Matplotlib configured with font list: {font_list[:3]}...")
|
||||||
|
self._matplotlib_configured = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Matplotlib font configuration failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _configure_qt(self) -> bool:
|
||||||
|
"""Configure Qt application with appropriate CJK fonts."""
|
||||||
|
try:
|
||||||
|
app = QCoreApplication.instance()
|
||||||
|
if not app:
|
||||||
|
self.logger.warning("No Qt application instance found - Qt font configuration skipped")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Get best available CJK font
|
||||||
|
cjk_font = self._get_best_cjk_font()
|
||||||
|
|
||||||
|
if cjk_font:
|
||||||
|
# Set application-wide font
|
||||||
|
font = QFont(cjk_font)
|
||||||
|
app.setFont(font)
|
||||||
|
self.logger.info(f"Qt configured with CJK font: {cjk_font}")
|
||||||
|
else:
|
||||||
|
self.logger.info("Qt using default system font (no specific CJK font found)")
|
||||||
|
|
||||||
|
self._qt_configured = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Qt font configuration failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_best_cjk_font(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Find the best available CJK font for the current system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str or None: Name of best available CJK font
|
||||||
|
"""
|
||||||
|
# Check custom fonts first
|
||||||
|
for font_name in self.loaded_fonts.keys():
|
||||||
|
if self._is_cjk_capable(font_name):
|
||||||
|
return font_name
|
||||||
|
|
||||||
|
# Check system fonts
|
||||||
|
current_system = platform.system()
|
||||||
|
system_fonts = self.system_font_fallbacks.get(current_system,
|
||||||
|
self.system_font_fallbacks['Linux'])
|
||||||
|
|
||||||
|
available_fonts = set(fm.get_font_names())
|
||||||
|
for font_name in system_fonts['sans-serif']:
|
||||||
|
if font_name in available_fonts:
|
||||||
|
return font_name
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _is_cjk_capable(self, font_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a font supports CJK characters.
|
||||||
|
Simple heuristic based on font name.
|
||||||
|
"""
|
||||||
|
cjk_indicators = [
|
||||||
|
'cjk', 'japanese', 'chinese', 'korean', 'han', 'noto', 'yu',
|
||||||
|
'hiragino', 'meiryo', 'gothic', 'mincho', '游', 'メイリオ',
|
||||||
|
'ゴシック', '明朝'
|
||||||
|
]
|
||||||
|
font_lower = font_name.lower()
|
||||||
|
return any(indicator in font_lower for indicator in cjk_indicators)
|
||||||
|
|
||||||
|
def get_cjk_safe_title(self, title: str, fallback_encoding: str = 'utf-8') -> str:
|
||||||
|
"""
|
||||||
|
Ensure title string is safe for display with current font configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: Original title string
|
||||||
|
fallback_encoding: Encoding to use for problematic characters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Safe title string for display
|
||||||
|
"""
|
||||||
|
if not title:
|
||||||
|
return title
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test if the string can be encoded/decoded properly
|
||||||
|
title.encode(fallback_encoding).decode(fallback_encoding)
|
||||||
|
return title
|
||||||
|
except UnicodeError:
|
||||||
|
# If there are encoding issues, create a safe fallback
|
||||||
|
safe_title = title.encode(fallback_encoding, errors='replace').decode(fallback_encoding)
|
||||||
|
self.logger.debug(f"Title encoding adjusted: {title[:50]}... -> {safe_title[:50]}...")
|
||||||
|
return safe_title
|
||||||
|
|
||||||
|
def create_fonts_directory_if_needed(self) -> Path:
|
||||||
|
"""
|
||||||
|
Create the fonts directory and return its path.
|
||||||
|
Useful for setup instructions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: Path to the fonts directory
|
||||||
|
"""
|
||||||
|
self.fonts_dir.mkdir(exist_ok=True)
|
||||||
|
return self.fonts_dir
|
||||||
|
|
||||||
|
def get_font_installation_instructions(self) -> str:
|
||||||
|
"""
|
||||||
|
Generate user instructions for installing CJK fonts.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Multi-line instruction string
|
||||||
|
"""
|
||||||
|
fonts_path = self.create_fonts_directory_if_needed()
|
||||||
|
|
||||||
|
instructions = f"""CJK Font Installation Instructions:
|
||||||
|
|
||||||
|
1. Create or use the fonts directory: {fonts_path.absolute()}
|
||||||
|
|
||||||
|
2. Download CJK fonts (legally) from sources like:
|
||||||
|
- Google Fonts (Noto Sans CJK, free & open source)
|
||||||
|
- Adobe Source Han fonts (free & open source)
|
||||||
|
- System fonts from your OS (if redistribution is allowed)
|
||||||
|
|
||||||
|
3. Place .ttf, .otf, or .ttc font files in the fonts/ directory
|
||||||
|
|
||||||
|
4. Restart the application to load the new fonts
|
||||||
|
|
||||||
|
Note: The fonts/ directory is gitignored to avoid licensing issues.
|
||||||
|
System CJK fonts will be used as fallback if available.
|
||||||
|
|
||||||
|
Current system: {platform.system()}
|
||||||
|
Recommended fonts: {', '.join(self.system_font_fallbacks.get(platform.system(), {}).get('sans-serif', ['System default'])[:3])}
|
||||||
|
"""
|
||||||
|
return instructions
|
||||||
|
|
||||||
|
def get_status_report(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generate a status report of the font system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Status information
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'matplotlib_configured': self._matplotlib_configured,
|
||||||
|
'qt_configured': self._qt_configured,
|
||||||
|
'custom_fonts_loaded': len(self.loaded_fonts),
|
||||||
|
'custom_font_families': list(self.loaded_fonts.keys()),
|
||||||
|
'fonts_directory_exists': self.fonts_dir.exists(),
|
||||||
|
'fonts_directory_path': str(self.fonts_dir.absolute()),
|
||||||
|
'current_matplotlib_fonts': plt.rcParams.get('font.sans-serif', [])[:5],
|
||||||
|
'current_system': platform.system()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Global font manager instance
|
||||||
|
_font_manager: Optional[FontManager] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_font_manager() -> FontManager:
|
||||||
|
"""
|
||||||
|
Get the global font manager instance.
|
||||||
|
Creates one if it doesn't exist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FontManager: Global font manager instance
|
||||||
|
"""
|
||||||
|
global _font_manager
|
||||||
|
if _font_manager is None:
|
||||||
|
_font_manager = FontManager()
|
||||||
|
return _font_manager
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_fonts() -> bool:
|
||||||
|
"""
|
||||||
|
Initialize the global font system.
|
||||||
|
Call this early in application startup.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful
|
||||||
|
"""
|
||||||
|
return get_font_manager().initialize()
|
||||||
|
|
||||||
|
|
||||||
|
def safe_title(title: str) -> str:
|
||||||
|
"""
|
||||||
|
Convenience function to get CJK-safe title.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: Original title
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Safe title for display
|
||||||
|
"""
|
||||||
|
return get_font_manager().get_cjk_safe_title(title)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
This directory is for custom CJK fonts to improve character rendering.
|
||||||
|
|
||||||
|
Supported formats: .ttf, .otf, .ttc
|
||||||
|
|
||||||
|
Recommended free fonts for CJK support:
|
||||||
|
- Noto Sans CJK (Google Fonts)
|
||||||
|
- Source Han Sans (Adobe)
|
||||||
|
|
||||||
|
The application will automatically detect and use fonts placed here.
|
||||||
|
For setup instructions, run: python setup_fonts.py
|
||||||
|
|
||||||
|
Note: Only add fonts you have proper licensing rights to distribute.
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
Logging configuration for the Audio Mastering Toolkit.
|
||||||
|
Provides CLI configurable logging with different verbosity levels.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(level: str = "INFO", log_to_file: bool = False) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Setup logging configuration for the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: Logging level (ERROR, WARN, INFO, DEBUG, TRACE)
|
||||||
|
log_to_file: Whether to also log to file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured logger instance
|
||||||
|
"""
|
||||||
|
# Convert level string to logging constant
|
||||||
|
level_map = {
|
||||||
|
'ERROR': logging.ERROR,
|
||||||
|
'WARN': logging.WARNING,
|
||||||
|
'WARNING': logging.WARNING,
|
||||||
|
'INFO': logging.INFO,
|
||||||
|
'DEBUG': logging.DEBUG,
|
||||||
|
'TRACE': 5 # Custom level below DEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add custom TRACE level
|
||||||
|
logging.addLevelName(5, 'TRACE')
|
||||||
|
|
||||||
|
numeric_level = level_map.get(level.upper(), logging.INFO)
|
||||||
|
|
||||||
|
# Create formatter
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
||||||
|
datefmt='%H:%M:%S'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Setup console handler
|
||||||
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
console_handler.setLevel(numeric_level)
|
||||||
|
|
||||||
|
# Setup root logger
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(numeric_level)
|
||||||
|
root_logger.handlers.clear() # Clear any existing handlers
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# Optional file logging
|
||||||
|
if log_to_file:
|
||||||
|
file_handler = logging.FileHandler('audio_analysis.log')
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
file_handler.setLevel(numeric_level)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Add trace method to all loggers
|
||||||
|
def trace(self, message, *args, **kwargs):
|
||||||
|
if self.isEnabledFor(5):
|
||||||
|
self._log(5, message, args, **kwargs)
|
||||||
|
|
||||||
|
logging.Logger.trace = trace
|
||||||
|
|
||||||
|
# Create main application logger
|
||||||
|
app_logger = logging.getLogger('audio_mastering')
|
||||||
|
app_logger.info(f"Logging initialized at {level.upper()} level")
|
||||||
|
|
||||||
|
return app_logger
|
||||||
|
|
||||||
|
|
||||||
|
def parse_log_args() -> tuple[str, bool]:
|
||||||
|
"""
|
||||||
|
Parse command line arguments for logging configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (log_level, log_to_file)
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(add_help=False) # Don't interfere with main arg parsing
|
||||||
|
parser.add_argument('--log-level', '-l',
|
||||||
|
choices=['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'],
|
||||||
|
default='INFO',
|
||||||
|
help='Set logging verbosity level')
|
||||||
|
parser.add_argument('--log-file', action='store_true',
|
||||||
|
help='Also log to audio_analysis.log file')
|
||||||
|
|
||||||
|
# Parse known args only (ignore others for main app)
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
return args.log_level, args.log_file
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||||
QHBoxLayout, QSplitter, QLabel, QListWidget,
|
QHBoxLayout, QSplitter, QLabel, QListWidget,
|
||||||
QTextEdit, QListWidgetItem)
|
QTextEdit, QListWidgetItem)
|
||||||
@@ -7,6 +8,8 @@ from PyQt5.QtCore import Qt
|
|||||||
|
|
||||||
from audio_visualization_widget import AudioVisualizationWidget
|
from audio_visualization_widget import AudioVisualizationWidget
|
||||||
from analysis_results_manager import AnalysisResultsManager
|
from analysis_results_manager import AnalysisResultsManager
|
||||||
|
from logger_setup import setup_logging, parse_log_args
|
||||||
|
from font_manager import initialize_fonts, get_font_manager
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
@@ -14,6 +17,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
self.analysis_manager = AnalysisResultsManager()
|
self.analysis_manager = AnalysisResultsManager()
|
||||||
self.initUI()
|
self.initUI()
|
||||||
self.connect_signals()
|
self.connect_signals()
|
||||||
@@ -81,6 +85,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.analysis_manager.analysisStarted.connect(self.on_analysis_started)
|
self.analysis_manager.analysisStarted.connect(self.on_analysis_started)
|
||||||
self.analysis_manager.analysisCompleted.connect(self.on_analysis_completed)
|
self.analysis_manager.analysisCompleted.connect(self.on_analysis_completed)
|
||||||
self.analysis_manager.analysisError.connect(self.on_analysis_error)
|
self.analysis_manager.analysisError.connect(self.on_analysis_error)
|
||||||
|
self.analysis_manager.progressUpdate.connect(self.on_progress_update)
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
"""Handle drag enter event for file drops."""
|
"""Handle drag enter event for file drops."""
|
||||||
@@ -99,13 +104,17 @@ class MainWindow(QMainWindow):
|
|||||||
files = [u.toLocalFile() for u in event.mimeData().urls()]
|
files = [u.toLocalFile() for u in event.mimeData().urls()]
|
||||||
audio_files = [f for f in files if f.lower().endswith(('.mp3', '.wav', '.flac'))]
|
audio_files = [f for f in files if f.lower().endswith(('.mp3', '.wav', '.flac'))]
|
||||||
|
|
||||||
|
self.logger.info(f"Files dropped: {len(files)} total, {len(audio_files)} audio files")
|
||||||
|
|
||||||
if audio_files:
|
if audio_files:
|
||||||
# Analyze the first audio file
|
# Analyze the first audio file
|
||||||
# TODO: Add support for multiple file queue
|
# TODO: Add support for multiple file queue
|
||||||
file_path = audio_files[0]
|
file_path = audio_files[0]
|
||||||
|
self.logger.info(f"Starting analysis of dropped file: {os.path.basename(file_path)}")
|
||||||
self.analysis_manager.analyze_file(file_path)
|
self.analysis_manager.analyze_file(file_path)
|
||||||
else:
|
else:
|
||||||
self.visualization_widget.set_status("No audio files detected in drop")
|
self.visualization_widget.set_status("No audio files detected in drop")
|
||||||
|
self.logger.warning("No supported audio files found in drop")
|
||||||
|
|
||||||
def on_analysis_started(self, file_path):
|
def on_analysis_started(self, file_path):
|
||||||
"""Called when analysis starts."""
|
"""Called when analysis starts."""
|
||||||
@@ -143,8 +152,14 @@ class MainWindow(QMainWindow):
|
|||||||
def on_analysis_error(self, file_path, error_message):
|
def on_analysis_error(self, file_path, error_message):
|
||||||
"""Called when analysis fails."""
|
"""Called when analysis fails."""
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
|
self.logger.error(f"Analysis failed for {filename}: {error_message}")
|
||||||
self.visualization_widget.set_status(f"Error analyzing {filename}: {error_message}")
|
self.visualization_widget.set_status(f"Error analyzing {filename}: {error_message}")
|
||||||
|
|
||||||
|
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}%)")
|
||||||
|
|
||||||
def on_file_selected(self, item):
|
def on_file_selected(self, item):
|
||||||
"""Called when a file is selected from the list."""
|
"""Called when a file is selected from the list."""
|
||||||
file_path = item.data(Qt.UserRole)
|
file_path = item.data(Qt.UserRole)
|
||||||
@@ -160,12 +175,34 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# Parse logging arguments before creating QApplication
|
||||||
|
log_level, log_to_file = parse_log_args()
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
# Set application style
|
# Set application style
|
||||||
app.setStyle('Fusion') # Modern cross-platform style
|
app.setStyle('Fusion') # Modern cross-platform style
|
||||||
|
logger.debug("Application style set to Fusion")
|
||||||
|
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
|
logger.info("GUI window displayed")
|
||||||
|
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
+14
-7
@@ -8,6 +8,7 @@ import matplotlib.cm as cm
|
|||||||
|
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
|
from font_manager import safe_title, initialize_fonts
|
||||||
|
|
||||||
def try_mp3_tags(file_path):
|
def try_mp3_tags(file_path):
|
||||||
try:
|
try:
|
||||||
@@ -20,19 +21,19 @@ def try_mp3_tags(file_path):
|
|||||||
|
|
||||||
def read_mp3_tags(file_path):
|
def read_mp3_tags(file_path):
|
||||||
if (audio := try_mp3_tags(file_path)) is not None:
|
if (audio := try_mp3_tags(file_path)) is not None:
|
||||||
print(f"File name: {os.path.basename(file_path)}")
|
print(f"File name: {safe_title(os.path.basename(file_path))}")
|
||||||
print(f"{audio['artist'][0]} - {audio['title'][0]}")
|
print(f"{safe_title(audio['artist'][0])} - {safe_title(audio['title'][0])}")
|
||||||
else:
|
else:
|
||||||
print(f"File name: {os.path.basename(file_path)}")
|
print(f"File name: {safe_title(os.path.basename(file_path))}")
|
||||||
|
|
||||||
class AudioFile:
|
class AudioFile:
|
||||||
def __init__(self, file_path):
|
def __init__(self, file_path):
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
# file name / song name
|
# file name / song name
|
||||||
if (audio := try_mp3_tags(self.file_path)) is not None:
|
if (audio := try_mp3_tags(self.file_path)) is not None:
|
||||||
self.song_name = f"{audio['artist'][0]} - {audio['title'][0]}"
|
self.song_name = safe_title(f"{audio['artist'][0]} - {audio['title'][0]}")
|
||||||
else:
|
else:
|
||||||
self.song_name = os.path.basename(self.file_path)
|
self.song_name = safe_title(os.path.basename(self.file_path))
|
||||||
|
|
||||||
self.y, self.sr = librosa.load(file_path)
|
self.y, self.sr = librosa.load(file_path)
|
||||||
# load automatically normalises everything to [-1.0, 1.0]
|
# load automatically normalises everything to [-1.0, 1.0]
|
||||||
@@ -49,7 +50,10 @@ class AudioFile:
|
|||||||
return self.max_amplitude, self.avg_amplitude
|
return self.max_amplitude, self.avg_amplitude
|
||||||
|
|
||||||
def get_bpm(self):
|
def get_bpm(self):
|
||||||
return self.bpm
|
# librosa.beat.beat_track returns numpy array - extract scalar value
|
||||||
|
if isinstance(self.bpm, np.ndarray):
|
||||||
|
return float(self.bpm[0]) if len(self.bpm) > 0 else 0.0
|
||||||
|
return float(self.bpm)
|
||||||
|
|
||||||
def get_energy_levels_over_time(self, window = 10, hop = 2):
|
def get_energy_levels_over_time(self, window = 10, hop = 2):
|
||||||
"""_summary_
|
"""_summary_
|
||||||
@@ -121,7 +125,7 @@ class AudioFile:
|
|||||||
|
|
||||||
ax.set_ylabel('Power')
|
ax.set_ylabel('Power')
|
||||||
ax.set_xlabel('Time')
|
ax.set_xlabel('Time')
|
||||||
ax.set_title(f'{os.path.basename(self.file_path)}')
|
ax.set_title(safe_title(os.path.basename(self.file_path)))
|
||||||
|
|
||||||
plt.show(block=False)
|
plt.show(block=False)
|
||||||
plt.pause(0.001)
|
plt.pause(0.001)
|
||||||
@@ -216,6 +220,9 @@ if __name__ == '__main__':
|
|||||||
print("For the new GUI interface, please run: python main.py")
|
print("For the new GUI interface, please run: python main.py")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
# Initialize fonts for matplotlib
|
||||||
|
initialize_fonts()
|
||||||
|
|
||||||
# Replace 'path/to/your/audiofile.mp3' with the path to your audio file
|
# Replace 'path/to/your/audiofile.mp3' with the path to your audio file
|
||||||
file_path = []
|
file_path = []
|
||||||
with open('./files.txt', 'r') as f:
|
with open('./files.txt', 'r') as f:
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "uj-mastering-master",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
+3
-2
@@ -9,6 +9,7 @@ import matplotlib.colors as mcolors
|
|||||||
import matplotlib.cm as cm
|
import matplotlib.cm as cm
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
import os
|
import os
|
||||||
|
from font_manager import safe_title
|
||||||
|
|
||||||
|
|
||||||
class PlottingEngine:
|
class PlottingEngine:
|
||||||
@@ -58,7 +59,7 @@ class PlottingEngine:
|
|||||||
# Labels and title
|
# Labels and title
|
||||||
ax.set_ylabel('Power')
|
ax.set_ylabel('Power')
|
||||||
ax.set_xlabel('Time (seconds)')
|
ax.set_xlabel('Time (seconds)')
|
||||||
ax.set_title(f'{os.path.basename(file_path)}')
|
ax.set_title(safe_title(os.path.basename(file_path)))
|
||||||
|
|
||||||
# Tight layout for better appearance in GUI
|
# Tight layout for better appearance in GUI
|
||||||
fig.tight_layout()
|
fig.tight_layout()
|
||||||
@@ -73,7 +74,7 @@ class PlottingEngine:
|
|||||||
Returns:
|
Returns:
|
||||||
str: Formatted metadata text
|
str: Formatted metadata text
|
||||||
"""
|
"""
|
||||||
return f"""Track: {song_name}
|
return f"""Track: {safe_title(song_name)}
|
||||||
BPM: {bpm:.1f}
|
BPM: {bpm:.1f}
|
||||||
Max Amplitude: {max_amplitude:.3f}
|
Max Amplitude: {max_amplitude:.3f}
|
||||||
Avg Amplitude: {avg_amplitude:.3f}"""
|
Avg Amplitude: {avg_amplitude:.3f}"""
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Font setup utility for CJK character support.
|
||||||
|
Provides installation instructions and system font detection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add current directory to path to import our modules
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from font_manager import get_font_manager
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main font setup utility."""
|
||||||
|
print("=== Audio Analysis Toolkit - CJK Font Setup ===\n")
|
||||||
|
|
||||||
|
# Get font manager and show current status
|
||||||
|
font_manager = get_font_manager()
|
||||||
|
|
||||||
|
print("Current System Information:")
|
||||||
|
print(f"Platform: {platform.system()} {platform.release()}")
|
||||||
|
print(f"Python: {platform.python_version()}\n")
|
||||||
|
|
||||||
|
# Try to initialize fonts
|
||||||
|
print("Initializing font system...")
|
||||||
|
success = font_manager.initialize()
|
||||||
|
|
||||||
|
# Show detailed status
|
||||||
|
status = font_manager.get_status_report()
|
||||||
|
print(f"Font system status: {'✓ OK' if success else '⚠ Issues detected'}")
|
||||||
|
print(f"Matplotlib configured: {'✓' if status['matplotlib_configured'] else '✗'}")
|
||||||
|
print(f"Qt configured: {'✓' if status['qt_configured'] else '✗'}")
|
||||||
|
print(f"Custom fonts loaded: {status['custom_fonts_loaded']}")
|
||||||
|
|
||||||
|
if status['custom_font_families']:
|
||||||
|
print(f"Custom font families: {', '.join(status['custom_font_families'])}")
|
||||||
|
|
||||||
|
print(f"Fonts directory: {status['fonts_directory_path']}")
|
||||||
|
print(f"Directory exists: {'✓' if status['fonts_directory_exists'] else '✗'}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Show current matplotlib font configuration
|
||||||
|
print("Current matplotlib font stack:")
|
||||||
|
for i, font in enumerate(status['current_matplotlib_fonts'][:8], 1):
|
||||||
|
print(f" {i}. {font}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Show installation instructions
|
||||||
|
print(font_manager.get_font_installation_instructions())
|
||||||
|
|
||||||
|
# Test CJK character handling
|
||||||
|
print("\n=== Testing CJK Character Handling ===")
|
||||||
|
test_strings = [
|
||||||
|
"English Title",
|
||||||
|
"日本語のタイトル", # Japanese
|
||||||
|
"中文标题", # Chinese
|
||||||
|
"한국어 제목", # Korean
|
||||||
|
"Test - テスト", # Mixed
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Testing font-safe title conversion:")
|
||||||
|
for test_str in test_strings:
|
||||||
|
safe_str = font_manager.get_cjk_safe_title(test_str)
|
||||||
|
status_indicator = "✓" if test_str == safe_str else "⚠"
|
||||||
|
print(f" {status_indicator} '{test_str}' -> '{safe_str}'")
|
||||||
|
|
||||||
|
print("\n=== Setup Complete ===")
|
||||||
|
if success:
|
||||||
|
print("Font system is ready for use!")
|
||||||
|
if status['custom_fonts_loaded'] == 0:
|
||||||
|
print("Consider adding CJK fonts to improve character display.")
|
||||||
|
else:
|
||||||
|
print("There were issues with font setup. Check the logs for details.")
|
||||||
|
print("The application will still work but CJK characters may not display correctly.")
|
||||||
|
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user