v0.52.0: volume normalization — every song plays at one loudness

Measured once per file with ffmpeg's ebur128 (cached on this machine),
applied as a gain on the output volume; the next songs are measured
ahead and an unmeasured one glides to its level. Toggle in Controls and
Preferences → General.
This commit is contained in:
2026-10-13 13:29:37 -07:00
parent 78338d84a7
commit 6178f5d0f7
8 changed files with 571 additions and 7 deletions
+11
View File
@@ -147,6 +147,17 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
each load, and on `refresh_volume()` (MainWindow calls that when the
playing track is edited). `Player.volume()` stays the bare slider value.
- **Volume normalization** (Round 83, `loudness.py`, Controls → Normalize
Volume / Preferences → General): each file's EBU R128 integrated loudness
from the `ffmpeg` CLI's `ebur128` (~0.4 s a song), cached by path + size +
mtime in `$XDG_CACHE_HOME/lintunes/loudness.json` (machine-local, never
the synced dir). `Player` turns `TARGET_LUFS − measured` into a dB gain in
`_output_volume` (`volume_with_gain`, linear amplitude through Qt's own
volume curve, capped at full). The playing song is measured urgently and
the next `LOOKAHEAD` ahead; an unmeasured one starts at the cache's median
and **glides** (never jumps) to its own gain. No DSP, no tags: Qt's
pipeline has no insert point, and none is needed.
- **`lintunes/player.py`** — `Player(QObject)` walks a queue through a swappable
**`PlaybackSink`**. Player owns the queue, shuffle walk, per-track start/stop
times and the play-count/scrobble bookkeeping; a sink owns only "make this file