d7782bb9d9
Profiled hot spots on a 4-min track and cut the worst offenders: - Remove BPM: librosa.beat.beat_track ran on every load (~3.7s) for a number no better than tapping by hand. Dropped from AudioFile + the metadata panel. - LUFS short-term: replace 474 per-window pyloudnorm.integrated_loudness calls with one K-weighting pass (reusing pyloudnorm's own filter coefficients) + a vectorised sliding mean-square. This is true *ungated* EBU R128 short-term (the old loop wrongly gated each 3s window). Integrated + LRA still use pyloudnorm's gated calls. ~3.8s -> ~1.9s. - PSR: reuse LUFS's short-term series (memoised on the AudioFile) + vectorised sample-peak. ~3.0s -> ~0.2s. - True Peak: oversample the whole signal once, then an O(N) running max over windows instead of per-window resample_poly. Bit-identical to the old loop (max|diff| 0.0000 dB). ~2.1s -> ~1.1s. - Crest Factor: peaks via the same O(N) running max (last per-window loop gone). lufs+psr+true_peak: ~9.2s -> ~3.2s, plus ~3.7s of BPM removed from every load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 KiB
12 KiB
uj-mastering-master
A custom mastering toolkit that provides metrics to evaluate audio masterings through visual analysis.
Current implementation
Core features
- Audio Analysis: Uses librosa to analyze audio files (MP3/WAV/FLAC support) at native sample rate (no resampling)
- Pluggable Metrics: Switchable visualizations (RMS Power, Waveform, LUFS, Crest Factor, PSR, True Peak, Spectrogram; DR next) via a
MetricABC - Metadata Extraction: Reads ID3 tags from MP3 files for better file identification
- Modular GUI Architecture: Complete PyQt5 interface with drag-and-drop and file dialog support
- Font Management: CJK-capable, fixed UI font (M PLUS 1 Code @ 10pt) with system fallback
- Threading & Logging: Robust background processing with detailed logging system
Technical stack
- Audio Processing: librosa, numpy
- Visualization: pyqtgraph — persistent, interactive (mouse zoom/pan, lin/log toggle, multi-dataset overlay). matplotlib remains only for its colormaps (consumed by pyqtgraph) and as a librosa dependency
- GUI Framework: PyQt5 with modular widget architecture
- Metadata: mutagen for audio tag reading
- Font Support: Custom font loading system with CJK fallback
Key components
main.py
- Complete GUI application with modular architecture
- Drag-and-drop and file dialog support for audio files
- Integrated font control system
- Real-time analysis display and file management
analysis_results_manager.py
- Background threading for audio analysis
- Caches both the loaded
AudioFileand per-metriccompute()output, so metric/font switches re-render from cache without reloading librosa - Progress tracking and error handling
audio_visualization_widget.py
- Persistent pyqtgraph plot — the PlotItem is reused across renders, never torn down, so mouse zoom/pan and scale toggles survive every redraw
show_specs([(label, PlotSpec), ...], view)draws one or more datasets onto the shared axes, assigning a distinct colour per dataset for overlay/compare- Spectrogram log-frequency is realised by resampling STFT rows onto a log grid
(
ImageItemis affine-only and won't follow a log axis) — see_render_heatmap
plotspec.py
- Backend-agnostic drawing descriptors:
Curve,Band,HLine,Heatmap,AxisSpec,PlotSpec, plus theViewState(recompute-free lin/log options) - The seam that decouples metrics from the plotting library: metrics emit intent, the renderer owns colour/layout/library specifics
font_manager.py
- Auto-detection of custom fonts from
fonts/directory; CJK fallbacks apply_fixed_font(family, size)locks the Qt app font (used at startup to pin the UI to M PLUS 1 Code @ 10pt, falling back to the system default if the family isn't found). There is no runtime font picker — the oldfont_control_widget.pywas removed as wasted panel space- pyqtgraph and the Qt widgets both read the app font, so this covers the plot too (M PLUS 1 Code has full Japanese coverage, so titles stay CJK-safe)
plot_control_widget.py
- Metric selector dropdown driven by the
metrics.METRICSregistry - Log-frequency toggle and a time-axis mode selector — Absolute (seconds) vs Relative (% of each track's own length) — both view-state, recompute-free
Refresh Plotbutton. Compare/overlay membership is the file-list checkboxes; reference lines have their own cluster
ref_line_widget.py
RefLineControlWidget: side-panel list of custom reference lines with Add / Edit… / Remove / Clear; a pure view over theRefLinePropslist the main window owns, emitting intentsRefLineDialog: edits one line's value, colour, line style, and tag- The plot draws each line with a triangle drag-handle; dragging writes the new
value back into the shared
RefLinePropsand refreshes the list
metrics.py
- Pluggable
MetricABC:compute(audio_file) -> data(heavy, worker thread, backend-neutral numpy/scalars) andbuild_spec(data, view) -> PlotSpec(cheap, GUI thread, view-aware). Metrics no longer touch the plotting library - Compute-time vs view-time split: scale (lin/log) is a
ViewStateargument tobuild_spec, so toggling it never recomputes - Current registry:
RMSPowerMetric— 10 s rolling RMS with adaptive colour scaleWaveformMetric— min/max envelope, fixed ±1.1 y-rangeLUFSMetric— true (ungated) EBU R128 short-term (3 s) computed via a single K-weighting pass (_short_term_lufs, reusing pyloudnorm's filter coefficients) + a vectorised sliding mean-square; integrated + LRA still come from pyloudnorm (one gated call each). ~2× faster than the old per-window loopCrestFactorMetric— 20·log10(peak/RMS) per 1 s window; peaks via O(N) running maxPSRMetric— sample-peak minus short-term LUFS (3 s window); reusesLUFSMetric's short-term series (memoised on theAudioFile), so PSR is near-free once LUFS is computedTruePeakMetric— 4× oversampled dBTP; the whole signal is oversampled once (scipy.signal.resample_poly) then an O(N) running max over windowsSpectrogramMetric— log-frequency STFT heatmap; adaptive hop caps time bins at ~4000,N_FFT=4096. Log/linear frequency is a view toggle
- Drop in new ones (DR, spectral balance) by appending an instance to
METRICS; return aPlotSpecfrombuild_spec(curves overlay automatically; heatmaps show one dataset at a time) - Note: the old matplotlib
_show_axis_extentsexact-endpoint tick labelling is gone with the matplotlib render path. If wanted back, it belongs in the renderer, applied uniformly to every metric — not per-metric
master_core.py
- Defines the
AudioFileclass: librosa loading, rolling RMS power. BPM detection was removed —librosa.beat.beat_trackcost ~3.7 s on every load for a number no better than tapping by hand - Loads at native sample rate (
librosa.load(..., sr=None)) so the full band is preserved — analysis runs ~2× heavier on 44.1/48 kHz files than the old 22050 Hz default, by design - No batch / CLI mode — all analysis is driven from
main.pyviaAnalysisResultsManager
Current analysis features
- Native-rate loading: full-band analysis up to the file's own nyquist
- RMS power analysis: 10-second rolling window with 2-second hops
- Adaptive colour mapping: Automatically adjusts scale based on detected headroom
- High dynamic range: 0-0.6 scale for loud masters
- Conservative mastering: 0-0.3 scale for quiet masters
- Loudness metrics: LUFS (ungated short-term + gated integrated + LRA), PSR, Crest Factor
- Peak analysis: True Peak (4× oversampled dBTP)
- Spectral view: log-frequency spectrogram heatmap over time
- Metadata display: Artist and title from audio tags
- Real-time visualization: Embedded matplotlib plots with font-aware rendering
GUI features
- File management: Drag-and-drop and file dialog for audio selection
- Compare/overlay: each analysed file has a checkbox; the ticked set is overlaid on one graph for the current metric (curve metrics overlay; the spectrogram shows one track at a time). Highlighting a row drives the metadata panel, independent of the overlay set
- Interactive plot: mouse drag-zoom, scroll-wheel zoom, pan, right-click menu
(pyqtgraph ViewBox); log/linear frequency toggle. Scroll zooms both axes;
Ctrl+scroll zooms time only, Shift+scroll zooms the value axis only
(
_AxisZoomViewBox); scrolling over an axis also zooms just that axis - Time-axis mode: a Relative-time toggle — off = seconds, on = 0-100% of each track's own length, so tracks of very different durations line up by position
- Custom reference lines: side-panel list (Add/Edit/Remove/Clear) of draggable horizontal markers with value/colour/style/tag; dragged via a triangle handle. Kept per metric (so switching metrics doesn't lose them) and expressed in the metric's own units — on the spectrogram they read and edit in Hz (the renderer converts Hz<->row index, since the heatmap y-axis is a row index)
- Plot control: Metric selector + log-frequency toggle + relative-time toggle
- refresh-plot button
- Analysis display: Real-time visualization with metadata panels
- Modular architecture: Self-contained widgets for easy layout management
Future development plans
Short-term (urgent)
- Plot control widget cluster (metric selector + Refresh Plot done; still TODO)
- Plot style controller (colormap, line vs bar, etc.)
- Foundation for mastering comparison features
Short-term (not urgent)
-
Enhanced metrics (plug new ones into
metrics.METRICS)- Dynamic range measurement (DR meter)
- Long-term average spectrum (LTAS) / tonal-balance curve
- Stereo metrics (correlation, mid/side) — needs
AudioFileto retain stereo
-
Interactive plot features (zoom/pan, axis-range select, lin/log done via pyqtgraph)
- GUI-controllable plotting styles (colormap, visualization type)
- Export analysis results to CSV/JSON
-
Advanced GUI controls
- Plot style customization interface
- Real-time axis range selection (zooming in/out)
- Interactive plot manipulation tools
-
Better looking UI
- Graphical loading bar
- Graphical logging text box
Mid-to-long-term (very not urgent)
-
Audio comparison system (multi-file overlay done via file-list checkboxes; each song has a stable palette colour keyed to its list row)
- Per-song colour picker: clickable swatch in the file list (overlay already
accepts a caller-supplied colour per dataset via
show_specs, so this is a UI + override-map addition, not a render change) - Reference vs. comparee designation (vs. flat overlay)
- Side-by-side track comparison interface (incl. spectrogram, which can't overlay)
- A/B testing for mastering versions
- Per-song colour picker: clickable swatch in the file list (overlay already
accepts a caller-supplied colour per dataset via
-
Distribution & deployment
- Self-contained executable releases
- Cross-platform packaging
- Installer creation and distribution
Future vision
-
Advanced analysis tools
- Spectral centroid and bandwidth analysis
- Stereo width measurements
- Transient detection and analysis
- Harmonic distortion detection
-
Professional features
- EBU R128 compliance checking
- Custom target curves
- Professional reporting formats
- Multi-format export capabilities
-
VST plugin development
- Real-time analysis during mixing/mastering
- Integration with DAWs
- Live feedback during production
Development notes
Dependencies
- librosa: Audio analysis and feature extraction
- numpy: Numerical computations
- scipy: Signal processing (true-peak polyphase oversampling, K-weighting filters, spectrogram log-frequency resample, O(N) running-max via ndimage)
- pyloudnorm: BS.1770 loudness (LUFS, LRA)
- pyqtgraph: Interactive plotting (zoom/pan, overlay, lin/log)
- matplotlib: Colormaps only (consumed by pyqtgraph) + librosa dependency
- mutagen: Audio metadata extraction
- PyQt5: GUI framework
Architecture considerations
- Three-stage split:
metrics.compute(heavy, worker thread, backend-neutral data) →metrics.build_spec(cheap, GUI thread, view-awarePlotSpec) →AudioVisualizationWidget.show_specs(pyqtgraph rendering, overlay, colours) - File path handling needs improvement for cross-platform compatibility
- Error handling should be enhanced for production use
- Consider moving from PyQt5 to PyQt6 or PySide for better licensing
Testing requirements
- Unit tests for audio analysis functions
- GUI component testing
- File format compatibility testing
- Performance testing with large audio files
Usage
Running the app
uv sync # one-time, after cloning
uv run ujm # launch the GUI
Optional flags (handled by logger_setup.parse_log_args):
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
- Interactive plot manipulation and style customization
- Audio file comparison features (reference vs. comparee)
- Self-contained executable releases