This commit is contained in:
Mikkeli Matlock
2026-05-28 14:05:34 +09:00
parent c466de62b2
commit cf901a5686
14 changed files with 1848 additions and 648 deletions
+2 -3
View File
@@ -1,5 +1,3 @@
files.txt
# Font directory - avoid licensing issues by not committing font files
# Keep the directory structure but ignore actual font files
fonts/*.ttf
@@ -8,10 +6,11 @@ fonts/*.ttc
# But preserve the placeholder file
!fonts/PLACE_YOUR_FONT_FILES_HERE
# Python cache
# Python cache and build artifacts
__pycache__/
*.pyc
*.pyo
*.egg-info/
# IDE files
.vscode/
+6 -6
View File
@@ -43,7 +43,7 @@ The font system is integrated at these key locations:
font_success = initialize_fonts()
```
#### Plot Titles (`plotting_engine.py`, `master_core.py`)
#### Plot Titles (`plotting_engine.py`)
```python
ax.set_title(safe_title(os.path.basename(file_path)))
```
@@ -59,7 +59,7 @@ song_name = safe_title(f"{audio['artist'][0]} - {audio['title'][0]}")
1. **Run the setup utility:**
```bash
python setup_fonts.py
uv run python setup_fonts.py
```
2. **For enhanced CJK support, add fonts to the `fonts/` directory:**
@@ -163,7 +163,7 @@ print(status)
### Test CJK Characters
```bash
python setup_fonts.py
uv run python setup_fonts.py
```
### Logging
@@ -202,13 +202,13 @@ Font system operations are logged at appropriate levels:
### Debug Commands
```bash
# Check font system status
python setup_fonts.py
uv run python setup_fonts.py
# Test with specific log level
python main.py --log-level DEBUG
uv run ujm --log-level DEBUG
# Test matplotlib font configuration
python -c "import matplotlib.pyplot as plt; print(plt.rcParams['font.sans-serif'])"
uv run python -c "import matplotlib.pyplot as plt; print(plt.rcParams['font.sans-serif'])"
```
## Conclusion
+16 -12
View File
@@ -43,9 +43,8 @@ A custom mastering toolkit that provides metrics to evaluate audio masterings th
- Auto-regeneration of plots when fonts change
#### `master_core.py`
- Core audio analysis functionality
- `AudioFile` class with comprehensive metrics extraction
- RMS power analysis and BPM detection
- Defines the `AudioFile` class: librosa loading, rolling RMS power, BPM detection
- No batch / CLI mode — all analysis is driven from `main.py` via `AnalysisResultsManager`
### Current analysis features
- **RMS power analysis**: 10-second rolling window with 2-second hops
@@ -146,16 +145,21 @@ A custom mastering toolkit that provides metrics to evaluate audio masterings th
## Usage
### Current usage
1. Run `python main.py` to launch the GUI application
2. Use "Open Audio File..." button or drag-and-drop audio files for analysis
3. Adjust font settings using the Font Settings panel
4. View real-time analysis results with embedded matplotlib plots
5. Select different analyzed files from the file list to compare results
### Running the app
```bash
uv sync # one-time, after cloning
uv run ujm # launch the GUI
```
### Legacy usage (batch Mode)
1. Add audio file paths to `files.txt`
2. Run `python master_core.py` for batch analysis
Optional flags (handled by `logger_setup.parse_log_args`):
```bash
uv run ujm --log-level DEBUG # ERROR | WARN | INFO | DEBUG | TRACE
uv run ujm --log-file # also write audio_analysis.log
```
The only entry point is `ujm` (defined in `pyproject.toml` as
`ujm = "main:main"`). The previous `files.txt` batch mode and the
`python master_core.py` workflow have been removed.
### Planned usage enhancements
1. Interactive plot manipulation and style customization
+46 -66
View File
@@ -1,78 +1,58 @@
# uj-mastering-master
Custom mastering toolkit providing comprehensive metrics to evaluate audio masterings through visual analysis.
Custom mastering toolkit providing visual metrics for evaluating audio masterings.
Developed with Claude Code assistance.
## Features
### Current Implementation
- **Complete GUI Application**: Modular PyQt5 interface with drag-and-drop and file dialog support
- **Real-time Analysis**: Background threading with embedded matplotlib visualization
- **Font Management**: CJK-compatible font system with custom font support from `fonts/` directory
- **Audio Support**: MP3, WAV, and FLAC file analysis
- **RMS Power Analysis**: Rolling window analysis with adaptive color mapping
- **Metadata Display**: Automatic extraction and display of audio tags and BPM
### Current
- **PyQt5 GUI**: drag-and-drop or file-dialog ingest of `.mp3`, `.wav`, `.flac`
- **RMS power analysis** on a 10 s rolling window with adaptive colour scale
- **BPM detection** via librosa
- **CJK-safe font system** with custom fonts loaded from `fonts/` (gitignored), system fallbacks, and a live font selector
- **Background analysis thread** so the UI stays responsive
- **Embedded matplotlib canvas** with auto-regenerated plots on font change
### 🚧 In Development
- **Plot Control Widgets**: Dedicated cluster for plot manipulation and style controls
- **LUFS Metrics**: Professional loudness measurement implementation
- **Interactive Plotting**: Real-time axis control and style customization
### Roadmap
See [CLAUDE.md](CLAUDE.md) for the full development roadmap. Near-term:
plot-control widget cluster, LUFS, dynamic range, interactive axis controls.
### 🔮 Planned Features
- **Audio Comparison**: Reference vs. comparee analysis for mastering evaluation
- **Advanced Metrics**: Dynamic range, spectral analysis, and professional standards compliance
- **Standalone Releases**: Self-contained executable distribution
## Quick start
This project uses [uv](https://docs.astral.sh/uv/). With uv installed:
```bash
uv sync
uv run ujm
```
`uv run ujm` is the only supported entry point — it boots the GUI.
### Logging flags
```bash
uv run ujm --log-level DEBUG # ERROR | WARN | INFO | DEBUG | TRACE
uv run ujm --log-file # also write audio_analysis.log
```
### Fonts
Drop `.ttf` / `.otf` / `.ttc` files into `fonts/` to get them in the font
selector. The directory is gitignored to avoid bundling licensed font data.
See [CJK_FONTS.md](CJK_FONTS.md) for details.
## Dependencies
- **Core**: `librosa`, `numpy`, `matplotlib`, `mutagen`
- **GUI**: `PyQt5`
- **Audio Processing**: Advanced librosa-based analysis pipeline
## Usage
### GUI Application (Recommended)
```bash
python main.py
```
- **Load Files**: Use "Open Audio File..." button or drag-and-drop
- **Font Control**: Adjust interface fonts and regenerate plots automatically
- **Analysis Display**: View real-time RMS power analysis with metadata
- **File Management**: Switch between analyzed files using the file list
### Command Line Analysis (Legacy)
```bash
# 1. Edit files.txt with your audio file paths
# 2. Run batch analysis
python master_core.py
```
`librosa`, `numpy`, `matplotlib`, `mutagen`, `PyQt5` — all pinned through
`uv.lock`. Python 3.10+.
## Architecture
### Modular Design
- **Self-contained Widgets**: Easy layout management and customization
- **Background Processing**: Non-blocking analysis with progress feedback
- **Signal-based Communication**: Clean separation between GUI and analysis logic
### Key Components
- `main.py`: Complete GUI application with modular architecture
- `font_control_widget.py`: Unified font management with plot regeneration
- `analysis_results_manager.py`: Threaded analysis with caching
- `audio_visualization_widget.py`: Embedded matplotlib with Qt integration
## Development Roadmap
### 🎯 Next Priority: Plot Control System
Moving from font-focused interface to comprehensive plot manipulation:
- Cluster plot controls (refresh, style, metric selection)
- Interactive axis range selection
- Real-time plot style customization
- Foundation for comparison features
### 🎵 Short-term Goals
- **LUFS Implementation**: Professional loudness standards
- **Plot Interactivity**: GUI-controlled visualization styles
- **Metric Selection**: Choose which analysis to display
### 🎼 Long-term Vision
- **Mastering Comparison**: Side-by-side analysis tools
- **Professional Standards**: EBU R128 compliance checking
- **Standalone Distribution**: Self-contained executable releases
| Module | Responsibility |
| --- | --- |
| `main.py` | `MainWindow` + the `ujm` entry point |
| `analysis_results_manager.py` | Background `QThread` worker, result cache |
| `master_core.py` | `AudioFile`: librosa loading, RMS rolling window, BPM |
| `plotting_engine.py` | Matplotlib `Figure` builder for the power graph |
| `audio_visualization_widget.py` | Embedded `FigureCanvasQTAgg` host |
| `font_manager.py` | Custom + system CJK font discovery, matplotlib/Qt config |
| `font_control_widget.py` | Font picker, size slider, refresh-plot button |
| `logger_setup.py` | CLI log-level parsing + custom TRACE level |
| `setup_fonts.py` | Diagnostic utility (run standalone) |
Binary file not shown.
+1 -1
View File
@@ -68,7 +68,7 @@ class AudioAnalysisWorker(QThread):
bpm=audio_file.get_bpm(),
max_amplitude=audio_file.max_amplitude,
avg_amplitude=audio_file.avg_amplitude,
times=audio_file._get_times(),
times=audio_file.get_times(),
rms_array=audio_file.rms_array,
analysis_successful=True
)
-52
View File
@@ -6,7 +6,6 @@ Pure display responsibility - receives plotting data and shows graphs.
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
class AudioVisualizationWidget(QWidget):
@@ -49,57 +48,6 @@ class AudioVisualizationWidget(QWidget):
ax.set_yticks([])
self.canvas.draw()
def display_analysis_figure(self, figure):
"""
Display a matplotlib figure in the widget.
Args:
figure: matplotlib.figure.Figure to display
"""
# Clear current figure
self.figure.clear()
# Copy the provided figure to our canvas
# Get the subplot from the provided figure
source_ax = figure.get_axes()[0]
# Create new subplot in our figure
ax = self.figure.add_subplot(111)
# Copy all the plot elements
for child in source_ax.get_children():
if hasattr(child, 'get_data'):
# Copy line plots
try:
x_data, y_data = child.get_data()
ax.plot(x_data, y_data, color=child.get_color(),
linewidth=child.get_linewidth())
except:
pass
# Copy collections (fill_between creates PolyCollection)
for collection in source_ax.collections:
ax.add_collection(collection)
# Copy axis properties
ax.set_xlim(source_ax.get_xlim())
ax.set_ylim(source_ax.get_ylim())
ax.set_xlabel(source_ax.get_xlabel())
ax.set_ylabel(source_ax.get_ylabel())
ax.set_title(source_ax.get_title())
# Copy colorbar if it exists
if hasattr(figure, '_colorbar') or len(figure.get_axes()) > 1:
# Try to copy colorbar
try:
cbar = figure.colorbar(source_ax.collections[-1], ax=ax, label='RMS Power')
except:
pass
self.figure.tight_layout()
self.canvas.draw()
self.status_label.setText("Analysis complete - displaying power graph")
def display_figure_direct(self, figure):
"""
Display a figure by replacing our canvas figure entirely.
-40
View File
@@ -1,40 +0,0 @@
#!/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()
-208
View File
@@ -1,208 +0,0 @@
"""
Font selector widget providing GUI interface to font management system.
Self-contained widget that can be placed anywhere in the layout.
"""
import logging
from typing import List, Dict, Optional
from PyQt5.QtWidgets import QWidget, QComboBox, QVBoxLayout, QLabel
from PyQt5.QtCore import pyqtSignal
from font_manager import get_font_manager
class FontSelectorWidget(QWidget):
"""
Self-contained font selector widget.
Provides a dropdown interface to select fonts from available
custom fonts and system fonts. Integrates with the FontManager
backend for font discovery and application.
"""
# Signal emitted when font selection changes
fontChanged = pyqtSignal(str, str) # (font_name, font_type)
def __init__(self, parent=None):
"""Initialize the font selector widget."""
super().__init__(parent)
self.logger = logging.getLogger(__name__)
self.font_manager = get_font_manager()
self.available_fonts: Dict[str, str] = {} # display_name -> actual_font_name
self.initUI()
self.refresh_font_list()
def initUI(self):
"""Initialize the user interface."""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) # Minimal margins for embedding
# Label
self.label = QLabel("Font:")
layout.addWidget(self.label)
# Font selector dropdown
self.font_combo = QComboBox()
self.font_combo.currentTextChanged.connect(self.on_font_changed)
layout.addWidget(self.font_combo)
def refresh_font_list(self):
"""Refresh the list of available fonts."""
self.logger.debug("Refreshing font list...")
self.available_fonts.clear()
try:
# Get custom fonts from font manager
custom_fonts = self.font_manager.loaded_fonts
# Get system font candidates
system_fonts = self._get_system_font_candidates()
# Clear combo box
self.font_combo.clear()
# Add custom fonts first (highest priority)
if custom_fonts:
for font_family, font_path in custom_fonts.items():
display_name = f"{font_family} (Custom)"
self.available_fonts[display_name] = font_family
self.font_combo.addItem(display_name)
self.logger.debug(f"Added custom font: {display_name}")
# Add system fonts
for font_name in system_fonts:
display_name = f"{font_name} (System)"
self.available_fonts[display_name] = font_name
self.font_combo.addItem(display_name)
self.logger.debug(f"Added system font: {display_name}")
# Add default option with actual system font name
system_font_name = self.font_manager.get_default_system_font_name()
default_name = f"Default ({system_font_name})"
self.available_fonts[default_name] = "default"
self.font_combo.addItem(default_name)
# Select startup font
self._select_startup_font()
self.logger.info(f"Font list refreshed: {len(self.available_fonts)} fonts available")
except Exception as e:
self.logger.error(f"Error refreshing font list: {e}")
# Fallback: add default option only
self.font_combo.clear()
self.font_combo.addItem("Default (System)")
self.available_fonts = {"Default (System)": "default"}
def _get_system_font_candidates(self) -> List[str]:
"""
Get list of available system fonts that are good candidates.
Returns:
List[str]: List of available system font names
"""
# Use font manager's enhanced system font discovery
return self.font_manager.get_available_system_fonts()[:10] # Limit to reasonable number
def _select_startup_font(self):
"""Select the appropriate font on startup."""
# Use font manager's startup font selection logic
startup_font_name, startup_font_type = self.font_manager.select_startup_font()
# Find the corresponding display name in our combo box
target_display_name = None
for display_name, actual_name in self.available_fonts.items():
if actual_name == startup_font_name:
target_display_name = display_name
break
# If we found the font, select it
if target_display_name:
index = self.font_combo.findText(target_display_name)
if index >= 0:
self.font_combo.setCurrentIndex(index)
self.logger.info(f"Startup font selected: {target_display_name}")
return
# Fallback to default if we couldn't find the startup font
# Find any item that starts with "Default ("
for i in range(self.font_combo.count()):
item_text = self.font_combo.itemText(i)
if item_text.startswith("Default ("):
self.font_combo.setCurrentIndex(i)
self.logger.info(f"Startup font fallback: {item_text}")
return
def on_font_changed(self, display_name: str):
"""Handle font selection change."""
if not display_name or display_name not in self.available_fonts:
return
actual_font_name = self.available_fonts[display_name]
# Determine font type
if "(Custom)" in display_name:
font_type = "custom"
elif "(System)" in display_name:
font_type = "system"
else:
font_type = "default"
self.logger.info(f"Font changed: {display_name} -> {actual_font_name} ({font_type})")
# Apply the font change
self._apply_font_change(actual_font_name, font_type)
# Emit signal for any external listeners
self.fontChanged.emit(actual_font_name, font_type)
def _apply_font_change(self, font_name: str, font_type: str):
"""Apply the font change to the application."""
try:
# Use font manager's centralized font application
success = self.font_manager.apply_font_selection(font_name, font_type)
if not success:
self.logger.warning(f"Font application may have failed: {font_name}")
except Exception as e:
self.logger.error(f"Error applying font change: {e}")
def get_current_font(self) -> tuple[str, str]:
"""
Get currently selected font.
Returns:
tuple: (font_name, font_type)
"""
display_name = self.font_combo.currentText()
if display_name in self.available_fonts:
actual_font_name = self.available_fonts[display_name]
if "(Custom)" in display_name:
font_type = "custom"
elif "(System)" in display_name:
font_type = "system"
else:
font_type = "default"
return actual_font_name, font_type
return "default", "default"
def set_font(self, font_name: str):
"""
Programmatically set the font selection.
Args:
font_name: Name of font to select
"""
# Find matching display name
for display_name, actual_name in self.available_fonts.items():
if actual_name == font_name:
index = self.font_combo.findText(display_name)
if index >= 0:
self.font_combo.setCurrentIndex(index)
return
self.logger.warning(f"Font not found in selector: {font_name}")
+5 -1
View File
@@ -239,7 +239,7 @@ class MainWindow(QMainWindow):
self.visualization_widget.set_status(f"Error regenerating plot: {e}")
if __name__ == '__main__':
def main():
# Parse logging arguments before creating QApplication
log_level, log_to_file = parse_log_args()
@@ -271,3 +271,7 @@ if __name__ == '__main__':
logger.info("GUI window displayed")
sys.exit(app.exec_())
if __name__ == '__main__':
main()
+26 -207
View File
@@ -1,54 +1,39 @@
import librosa
import numpy as np
import os
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.cm as cm
import librosa
import numpy as np
from mutagen.mp3 import MP3
from mutagen.easyid3 import EasyID3
from font_manager import safe_title, initialize_fonts
def try_mp3_tags(file_path):
from font_manager import safe_title
def _try_mp3_tags(file_path):
try:
# if there is metadata
audio = MP3(file_path, ID3=EasyID3)
return audio
except Exception as e:
print(f"Error reading ID3 tags: {e}")
return MP3(file_path, ID3=EasyID3)
except Exception:
return None
def read_mp3_tags(file_path):
if (audio := try_mp3_tags(file_path)) is not None:
print(f"File name: {safe_title(os.path.basename(file_path))}")
print(f"{safe_title(audio['artist'][0])} - {safe_title(audio['title'][0])}")
else:
print(f"File name: {safe_title(os.path.basename(file_path))}")
class AudioFile:
def __init__(self, file_path):
self.file_path = file_path
# file name / song name
if (audio := try_mp3_tags(self.file_path)) is not None:
self.song_name = safe_title(f"{audio['artist'][0]} - {audio['title'][0]}")
audio = _try_mp3_tags(self.file_path)
artist = audio.get('artist', [None])[0] if audio is not None else None
title = audio.get('title', [None])[0] if audio is not None else None
if artist and title:
self.song_name = safe_title(f"{artist} - {title}")
else:
self.song_name = safe_title(os.path.basename(self.file_path))
# librosa.load normalises to [-1.0, 1.0]
self.y, self.sr = librosa.load(file_path)
# load automatically normalises everything to [-1.0, 1.0]
# and that's alright
self.y_mono = librosa.to_mono(self.y)
self.max_amplitude = np.max(np.abs(self.y_mono))
self.avg_amplitude = np.mean(np.abs(self.y_mono))
self.bpm, _ = librosa.beat.beat_track(y=self.y_mono, sr=self.sr)
def display_song_name(self):
print(self.song_name)
def get_amplitudes(self):
return self.max_amplitude, self.avg_amplitude
def get_bpm(self):
# librosa.beat.beat_track returns numpy array - extract scalar value
if isinstance(self.bpm, np.ndarray):
@@ -56,191 +41,25 @@ class AudioFile:
return float(self.bpm)
def get_energy_levels_over_time(self, window=10, hop=2):
"""_summary_
"""Compute rolling RMS power.
Args:
window (int, optional): Length of rolling RMS window in seconds. Defaults to 10.
hop (int, optional): Length of window hop in seconds. Defaults to 2.
window: Rolling window length in seconds.
hop: Hop length in seconds.
"""
# check if the window and hop are the same as before
if (not hasattr(self, 'window')) or ((self.window != window) or (self.hop != hop)):
if (not hasattr(self, 'window')) or (self.window != window) or (self.hop != hop):
self.window, self.hop = window, hop
# only calculate if not already calculated
if not hasattr(self, 'rms_array'):
# window and hop are in seconds
window_samples = window * self.sr
hop_samples = hop * self.sr
self.rms_array = librosa.feature.rms(
y=self.y, frame_length=window_samples, hop_length=hop_samples
)
# Calculate RMS over the rolling windows
self.rms_array = librosa.feature.rms(y=self.y, frame_length=window_samples, hop_length=hop_samples)
def _get_times(self):
"""Get time array for RMS data. Internal method for GUI integration."""
def get_times(self):
"""Time-axis values matching the RMS frames."""
if not hasattr(self, 'rms_array'):
self.get_energy_levels_over_time()
return librosa.frames_to_time(np.arange(self.rms_array.shape[1]), sr=self.sr, hop_length=self.hop*self.sr)
def plot_energy_levels_over_time(self, display='window'):
"""_summary_
Args:
display (str, optional): Option for where to display the plot. Defaults to 'window'.
'window' - display in a pyplot window
'gui' - for directing to the GUI (TBD)
"""
if not hasattr(self, 'rms_array'):
self.get_energy_levels_over_time()
# Convert frame indices to time
times = librosa.frames_to_time(np.arange(self.rms_array.shape[1]), sr=self.sr, hop_length=self.hop*self.sr)
# Normalize RMS for color mapping
# check maximum power to determine mastering headspace:
# a -6 dBFS headroom should yield a max power of around 0.25
# otherwise could go anywhere, but we take 0.6
local_max_power = np.max(self.rms_array)
if local_max_power > 0.3:
norm = mcolors.Normalize(vmin=0, vmax=0.6)
maxpower = 0.6
else:
norm = mcolors.Normalize(vmin=0, vmax=0.3)
maxpower = 0.3
# colour map
cmap = cm.autumn
# Plot
if display == 'window':
fig, ax = plt.subplots(figsize=(10, 4))
ax.set_ylim(0., maxpower)
for i in range(len(times)-1):
ax.fill_between(times[i:i+2], 0, self.rms_array[0][i], color=cmap(norm(self.rms_array[0][i])), edgecolor='none')
# Adding a colorbar to indicate the scale of RMS values
sm = cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
cbar = plt.colorbar(sm, ax=ax, label='RMS Power')
# cbar.ax.set_yticklabels([f"{x-60.0:.0f} dBFS" for x in cbar.get_ticks()]) # Adjust labels to show true dBFS values
ax.set_ylabel('Power')
ax.set_xlabel('Time')
ax.set_title(safe_title(os.path.basename(self.file_path)))
plt.show(block=False)
plt.pause(0.001)
def analyze_track_librosa(file_path):
# Load the audio file
# y is the audio time series and sr is the sampling rate
y, sr = librosa.load(file_path)
# Calculate the maximum amplitude
# Librosa's load function normalizes the audio to [-1, 1], so we scale it back
max_amplitude = np.max(np.abs(y))
# Average amplitude
avg_amplitude = np.mean(np.abs(y))
# Convert max amplitude to dBFS
max_amplitude_dBFS = librosa.amplitude_to_db([max_amplitude], ref=1.0)
avg_amplitude_dBFS = librosa.amplitude_to_db([avg_amplitude], ref=1.0)
# Calculate RMS in dB
S, phase = librosa.magphase(librosa.stft(y))
rms_stft = librosa.feature.rms(S=S)
rms = librosa.feature.rms(y=y)
avg_power_dBFS_stft = 20 * np.log10(np.mean(rms_stft))
avg_power_dBFS = 20 * np.log10(np.mean(rms))
return max_amplitude_dBFS[0], avg_amplitude_dBFS[0], avg_power_dBFS, avg_power_dBFS_stft
def plot_macro_time_power_graph(file_path):
# Load the audio file
y, sr = librosa.load(file_path, mono=True)
# Define the window and hop length
# 10 seconds window and 1 second hop
window_length = int(sr * 10) # 10 seconds in samples
hop_length = int(sr * 1) # 1 second in samples
# Calculate RMS over the rolling windows
rms = librosa.feature.rms(y=y, frame_length=window_length, hop_length=hop_length)
# Convert frame indices to time
times = librosa.frames_to_time(np.arange(rms.shape[1]), sr=sr, hop_length=hop_length)
# Normalize RMS for color mapping
norm = mcolors.Normalize(vmin=0, vmax=0.4)
# Choose a colormap
cmap = cm.autumn
# Plot
fig, ax = plt.subplots(figsize=(10, 4))
ax.set_ylim(0., 0.4)
for i in range(len(times)-1):
ax.fill_between(times[i:i+2], 0, rms[0][i], color=cmap(norm(rms[0][i])), edgecolor='none')
# Adding a colorbar to indicate the scale of RMS values
sm = cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
cbar = plt.colorbar(sm, ax=ax, label='RMS Power')
# cbar.ax.set_yticklabels([f"{x-60.0:.0f} dBFS" for x in cbar.get_ticks()]) # Adjust labels to show true dBFS values
ax.set_ylabel('Power')
ax.set_xlabel('Time')
# ax.set_title(f'{os.path.basename(file_path)}')
# plt.ylabel('Power')
# plt.xlabel('Time (s)')
# plt.title(f'{os.path.basename(file_path)}')
plt.show(block=False)
plt.pause(0.001)
def find_mp3_files(directory):
mp3_files = []
# Walk through the directory
for root, dirs, files in os.walk(directory):
# Filter and append .mp3 files
for file in files:
if file.endswith(".mp3"):
mp3_files.append(os.path.join(root, file))
return mp3_files
if __name__ == '__main__':
# Legacy batch processing mode - runs when master_core.py is executed directly
# For GUI usage, run main.py instead
print("Running legacy batch analysis mode...")
print("For the new GUI interface, please run: python main.py")
print()
# Initialize fonts for matplotlib
initialize_fonts()
# Replace 'path/to/your/audiofile.mp3' with the path to your audio file
file_path = []
with open('./files.txt', 'r') as f:
for line in f:
if line[0] != '#' and line[0] != ';':
file_path.append(line.strip())
for file in file_path:
# max_amplitude, avg_amplitude, avg_power, avg_power_stft = analyze_track_librosa(file)
# # read_mp3_tags(file)
# print(f"Maximum Amplitude: {max_amplitude:.2f} dBFS")
# print(f"Average Amplitude: {avg_amplitude:.2f} dBFS")
# print(f"Average Power: {avg_power:.2f} dBFS")
# print(f"Average Power (STFT): {avg_power_stft:.2f} dBFS")
currentsong = AudioFile(file)
currentsong.display_song_name()
print(f"BPM: {currentsong.get_bpm()}")
currentsong.plot_energy_levels_over_time()
# plot_macro_time_power_graph(file)
plt.show()
return librosa.frames_to_time(
np.arange(self.rms_array.shape[1]), sr=self.sr, hop_length=self.hop * self.sr
)
-6
View File
@@ -1,6 +0,0 @@
{
"name": "uj-mastering-master",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+34
View File
@@ -0,0 +1,34 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "uj-mastering-master"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"librosa",
"numpy",
"matplotlib",
"mutagen",
"PyQt5>=5.15.10",
# 5.15.2 is the only pyqt5-qt5 release with a Windows wheel; later
# versions are Linux/macOS only.
"PyQt5-Qt5==5.15.2 ; sys_platform == 'win32'",
]
[project.scripts]
ujm = "main:main"
[tool.setuptools]
py-modules = [
"main",
"analysis_results_manager",
"audio_visualization_widget",
"master_core",
"plotting_engine",
"font_manager",
"font_control_widget",
"logger_setup",
"setup_fonts",
]
Generated
+1666
View File
File diff suppressed because it is too large Load Diff