fix: copy albums whole when a re-level changes only their tags
Build and publish container / build (pull_request) Successful in 1m43s

A ReplayGain album gain belongs to the whole record, so a track arriving or
leaving rewrites the tags on every one of its siblings. rsgain fits the new
values into the padding its previous write left behind, which changes neither
the file's size nor its mtime:

  before: 277757 bytes, mtime 1577880000, album gain 3.75 dB
  after:  277757 bytes, mtime 1577880000, album gain 6.25 dB

Those are the two things rsync's quick check compares, so the siblings are
invisible to it and the device keeps the old gains indefinitely.

The track that arrived or left is always visible. So sync-to-ipod.sh now runs a
second pass over the albums the first one touched, with --ignore-times to
defeat the same quick check. touched_albums.py derives the list from rsync's
own report of what it moved, which costs no extra traversal of either tree, and
leaves out the tracks the first pass has already copied so a whole-album
quality upgrade is not sent twice. Albums whose file set has not changed are
left alone.

A dry run reports how many further tracks are involved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Emma Thorpe
2026-08-28 17:05:08 +01:00
co-authored by Claude Opus 5
parent c63115f246
commit 9a3b4f9955
5 changed files with 393 additions and 6 deletions
+39 -5
View File
@@ -197,6 +197,16 @@ flight; only the two figures that needed the second walk are missing.
Piped to a log it prints a plain line every thirty seconds instead, with no
carriage returns, and a summary at the end either way.
### Albums that changed are copied whole
After the main pass, any album that gained or lost a track has the rest of its
tracks copied again with `--ignore-times`. Their ReplayGain tags were rewritten
in place when the album was re-levelled, which changes neither their size nor
their mtime — the only two things rsync compares — so nothing else would ever
send them. `touched_albums.py` works out the list from rsync's own report of
what it moved, so this costs no extra traversal, and tracks the main pass has
already copied are left out of it. See [Volume levelling](#volume-levelling).
### The Rockbox database
Point `MUSIC_MIRROR_DATABASE_TOOL` at Rockbox's host-side builder and the sync
@@ -471,11 +481,16 @@ one.
Turn it on at the player end under **Settings → Playback Settings →
Replaygain**:
| Setting | Suggested | Why |
| ---------------- | -------------------- | --------------------------------------------------------- |
| Replaygain type | `Album Gain`, or `Track Gain if Shuffling` | Keeps a record's own dynamics; the second switches per mode |
| Prevent clipping | `Yes` | Uses the peak tags to back off rather than distort |
| Pre-amp | `0 dB` | The reference level is already 18 LUFS; move it only if the result is too quiet |
| Setting | Suggested | Why |
| ---------------- | ---------------------------- | ---------------------------------------------------------- |
| Replaygain type | `Track Gain if Shuffling` | Album gain while playing a record, track gain once shuffle is on — the only setting that is right in both cases |
| Prevent clipping | `Yes` | Backs the gain off using the peak tags rather than distorting |
| Pre-amp | `0 dB` | The reference is already 18 LUFS; raise it only if everything ends up too quiet |
`Track Gain if Shuffling` is not a compromise between the other two — Rockbox
reads the shuffle setting and picks whole-hog album or track gain from it
(`apps/misc.c`, `replaygain_setting_mode`). It is also Rockbox's default, so on
a fresh install there may be nothing to change but `Prevent clipping`.
Measuring costs a full decode of every track, so the first pass after enabling
it takes roughly as long as the original encode did. After that only albums
@@ -488,6 +503,25 @@ Tagging rewrites the file, and staleness here is an mtime comparison, so
look newer than its source and the next pass would re-encode the entire
library.
That has a consequence for the sync, and it is not obvious. The first time a
track is levelled its tag grows by about a kilobyte, so its size changes and
rsync copies it — the whole library goes across once, and there is no way
around that; the tag sits at the head of the file and every byte after it
moves. But `rsgain` leaves padding behind, so a *later* re-level fits inside it
and changes neither the size nor the mtime:
```
before re-level: 277757 bytes, mtime 1577880000, album gain 3.75 dB
after re-level: 277757 bytes, mtime 1577880000, album gain 6.25 dB
```
Those are the two things rsync's quick check compares, so it would see nothing
to do and the device would keep the old gains. `sync-to-ipod.sh` handles it: the
track that arrived or left is always visible, so any album the main pass
touched has the rest of its tracks copied again with `--ignore-times`. Albums
whose file set has not changed are not touched, which is what keeps this from
being a full re-copy.
## Getting the result onto an iPod
The mirror is just a directory of MP3s, so any client will do: