Files
uj-mastering-master/README.md
T

143 lines
6.4 KiB
Markdown
Raw Normal View History

2024-04-10 20:14:02 +09:00
# uj-mastering-master
2026-05-28 14:05:34 +09:00
Custom mastering toolkit providing visual metrics for evaluating audio masterings.
2025-08-22 00:26:27 +09:00
Developed with Claude Code assistance.
## Features
2026-05-28 14:05:34 +09:00
### Current
- **PyQt5 GUI**: drag-and-drop or file-dialog ingest of `.mp3`, `.wav`, `.flac`
- **Switchable metrics** via a dropdown, all sharing one analysis cache:
- **RMS Power** — 10 s rolling window with adaptive colour scale
- **Waveform** — min/max envelope, fixed ±1.1 scale
- **LUFS** — BS.1770 short-term (3 s) + integrated + loudness range (LRA)
- **Crest Factor** — peak-to-RMS spread over time
- **PSR** — peak-to-short-term-loudness ratio ("is it still breathing?")
- **True Peak** — 4× oversampled dBTP, catches inter-sample peaks
- **Spectrogram** — log-frequency STFT power heatmap over time
- **Always-labelled axis extremes**: every plot forces its exact min/max onto
the ticks, so you can read the true range even on a log axis (e.g. the
spectrogram's 22 kHz top, which otherwise falls between decade ticks)
- **Native sample rate**: audio is loaded without resampling, so the full band
(up to the file's own nyquist, e.g. ~22 kHz for 44.1 kHz files) is analysed
2026-05-28 14:05:34 +09:00
- **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; metric switches
compute off the GUI thread and cache, so re-selecting a metric is instant
2026-05-28 14:05:34 +09:00
- **Embedded matplotlib canvas** with auto-regenerated plots on font change
2025-08-22 00:26:27 +09:00
2026-05-28 14:05:34 +09:00
### Roadmap
See [CLAUDE.md](CLAUDE.md) for the full development roadmap. Near-term:
dynamic range (DR meter), plot-style controls, interactive axis controls.
2025-08-22 00:26:27 +09:00
2026-05-28 14:05:34 +09:00
## Quick start
2025-08-22 00:26:27 +09:00
2026-05-28 14:05:34 +09:00
This project uses [uv](https://docs.astral.sh/uv/). With uv installed:
2025-08-22 00:26:27 +09:00
```bash
2026-05-28 14:05:34 +09:00
uv sync
uv run ujm
2025-08-22 00:26:27 +09:00
```
2026-09-14 01:44:48 +09:00
`uv run ujm` boots the desktop GUI (see *Web UI* for the browser front end).
2026-05-28 14:05:34 +09:00
### Logging flags
2025-08-22 00:26:27 +09:00
```bash
2026-05-28 14:05:34 +09:00
uv run ujm --log-level DEBUG # ERROR | WARN | INFO | DEBUG | TRACE
uv run ujm --log-file # also write audio_analysis.log
2025-08-22 00:26:27 +09:00
```
2026-05-28 14:05:34 +09:00
### 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.
2025-08-22 00:26:27 +09:00
2026-09-14 01:44:48 +09:00
## Web UI
The same analysis backend with a browser front end, on branch `feature/web-ui`.
No PyQt5, no matplotlib: `metrics.py` was already split into a backend-neutral
`compute` and a view-aware `build_spec`, so the web UI reuses the analysis code
unchanged and reimplements only the drawing.
```bash
uv sync --extra web
uv run ujm-web # http://127.0.0.1:8000
uv run ujm-web --host 0.0.0.0 --port 8080 --open
```
Everything the desktop app does works the same way: all seven metrics, the
overlay, lin/log toggles, absolute/relative time, and draggable reference lines
(kept per metric, as on the desktop).
**Uploaded audio is never written to the host.** The browser copies each file
into Origin Private File System — its own sandboxed storage — and streams the
bytes to the server for one request at a time. The server spools each body to a
temp file, decodes, computes, and unlinks it in a `finally` before the response
is sent. There is no upload directory, no cache of audio, and no session state
keyed on a file path: `file_id` is the SHA-256 of the audio, so re-sending the
same master for a second metric reuses the first decode.
Being precise about the guarantee: the bytes do have to reach the server, because
`librosa` decodes from a path and the analysis is CPU-bound numpy/scipy.
*Running this does not accumulate the user's masters on the host* — nothing
persists between requests. It is not a claim that audio is never on the machine
mid-request.
The front end is served entirely from `static/` with uPlot vendored locally, so
nothing is fetched from a CDN, and the server binds loopback by default.
### Verifying it
With a server running:
```bash
python3 scripts/webapi_smoke.py # 61 assertions, needs ffmpeg for the fixtures
python3 scripts/webapi_smoke.py http://host:port /path/to/audio
```
The suite drives the real HTTP surface with real audio and checks the contract
the browser depends on — payload shapes per metric, physics cross-checked
against an independent `librosa` computation, view toggles that must not alter
data, error paths, and that no spool file survives a request.
### Web architecture
| Module | Responsibility |
| --- | --- |
| `web_session.py` | Headless analysis session: content-addressed track cache, metric cache, overlay |
| `metrics_json.py` | `PlotSpec` → JSON (the renderer seam; the desktop renderer is the other consumer) |
| `webapp.py` | FastAPI routes, raw-body analysis endpoints, static mount, `ujm-web` entry point |
| `static/audio-store.js` | OPFS audio store, content hashing, streamed upload |
| `static/app.js` | Web renderer: same `PlotSpec` drawn with uPlot + canvas |
2026-05-28 14:05:34 +09:00
## Dependencies
`librosa`, `numpy`, `matplotlib`, `mutagen`, `pyloudnorm`, `PyQt5` — all pinned
through `uv.lock`. Python 3.10+.
2025-08-22 00:26:27 +09:00
2026-09-14 01:44:48 +09:00
The web UI additionally needs `fastapi`, `uvicorn` and `python-multipart`
(`uv sync --extra web`).
2026-05-28 14:05:34 +09:00
## Architecture
2025-08-22 00:26:27 +09:00
2026-05-28 14:05:34 +09:00
| Module | Responsibility |
| --- | --- |
| `main.py` | `MainWindow` + the `ujm` entry point |
| `analysis_results_manager.py` | Background `QThread` worker, result + metric-data cache |
| `master_core.py` | `AudioFile`: native-rate librosa loading, RMS rolling window, BPM |
| `metrics.py` | Pluggable `Metric` ABC + registry (RMS, Waveform, LUFS, Crest, PSR, True Peak, Spectrogram) |
2026-05-28 14:05:34 +09:00
| `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 |
| `plot_control_widget.py` | Metric selector + refresh-plot button |
2026-05-28 14:05:34 +09:00
| `logger_setup.py` | CLI log-level parsing + custom TRACE level |
| `setup_fonts.py` | Diagnostic utility (run standalone) |
### Adding a metric
Subclass `Metric` in `metrics.py`, implement `compute(audio_file) -> data` (the
2026-09-14 01:44:48 +09:00
heavy part, runs on the worker thread) and `build_spec(data, view) -> PlotSpec`
(cheap, view-aware, reruns on every scale toggle without recomputation). Register
the instance in the `METRICS` dict at the bottom of the file — it shows up in the
desktop dropdown and in the web UI's metric list automatically, because both
renderers consume the same `PlotSpec`.