Skip to content

Say what a file's audio actually is, on the page built to repair it - #57

Closed
revtex wants to merge 1 commit into
normalize-metadata-page-eolfrom
metadata-quality-badge
Closed

Say what a file's audio actually is, on the page built to repair it#57
revtex wants to merge 1 commit into
normalize-metadata-page-eolfrom
metadata-quality-badge

Conversation

@revtex

@revtex revtex commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Stacked on #56. Base is normalize-metadata-page-eol, not main, so this diff shows 22 added lines in MetadataPage.xaml instead of 1768 changed ones. GitHub retargets this to main automatically once #56 merges. Merge #56 first.

The Metadata page reviews a folder of files and repairs what their tags say. It never said anything about the audio underneath those tags, so a 96 kbps download and a lossless rip presented identically on the one page where a whole library passes under review — the place where noticing would cost nothing, because the user is already looking at every row.

Each row now carries a line under its file name: MP3 · 320 kbps, FLAC · Lossless.

Why not the spectral approach

The obvious way to answer this is the wrong size for the question. A recorder in the same space does it by decoding the samples and running an FFT over them, hunting the brick-wall low-pass a lossy encoder leaves behind, and it earns that: a spectral cut-off is the only thing that catches a lossy file rewrapped as FLAC, where the container is lying. That is a real feature and a much larger one — a DSP pass per file, a confidence model, and a verdict that can be wrong.

This is the smaller, honest half: what does the file itself claim. No decoding, no ffprobe, no new package.

How

TagLib# already has it. The scan opens every file to read its tags, and the same File object carries Properties — bitrate, sample rate, bit depth — so the answer costs one more open per row.

Lossless is decided by bit depth, not codec name. TagLib# populates BitsPerSample only for codecs that have a fixed one, which is exactly the lossless set, and reports zero for MP3, AAC, Opus and Vorbis, none of which have one. That beats matching a list of codec strings that has to stay right about formats nobody here has seen.

Tiers: Lossless (bit depth > 0), then by bitrate — >=256 High, 128–255 Medium, <128 Low.

Decisions worth flagging in review

  • The reader is its own seam, not an extra return value on ILibraryTagStore. That interface is heavily covered and exists so a scan can be tested without a real audio file for every case; widening Read to return a pair would have rewritten those tests to carry a value none of them care about. The cost is one more TagLib# open per file during a user-initiated scan, on a folder measured in thousands.
  • LibraryTrack takes quality as an optional third argument, so every existing two-argument construction — nearly all of it in tests — keeps compiling untouched.
  • Only the low tier is coloured. A row that announces itself in amber for being 320 kbps is a warning about nothing, and a page where most rows are coloured has taught the user to stop reading the colour. Lossy under 128 kbps is the one reading worth a second look.
  • A lossless .m4a is labelled ALAC, not AAC. The extension cannot answer that on its own; the tier already knows.
  • TagLibAudioQualityReaderTests encodes with BitrateMode.Constant, and that is not incidental. The shared fixture's source is a two-second 440 Hz sine — trivially compressible — and ABR allocates on spectral demand, so a request for 320 kbps came back under 128 and the tier assertion failed on encoder behaviour that was entirely correct. The plan already records the neighbouring version of this trap (pink noise being the wrong thing to measure ABR on). CBR makes the reading deterministic.

Testing

  • New: AudioQualityTests (tier thresholds and boundaries), TagLibAudioQualityReaderTests (Ffmpeg-tagged, against real encoded files).
  • Extended: LibraryScannerTests (quality reaches the row), LibraryTrackViewModelTests (label text, caution flag, ALAC-vs-AAC naming, absent-when-unread).
  • 1039 Core + 287 UI tests pass. en/fr resource key parity holds.

⚠️ Not visually verified. The badge is covered by ViewModel tests and the app launches without crashing, but nobody has looked at the rendered window — scanning a folder with a low-bitrate MP3 and a FLAC in it is the check that would catch a binding typo or the caution colour not firing.

🤖 Generated with Claude Code

The Metadata page reviews a folder of files and repairs what their tags
say. It never said anything about the audio underneath those tags, so a
96 kbps download and a lossless rip presented identically on the one page
where a whole library passes under review — the place where noticing
would cost nothing, because the user is already looking at every row.

The obvious way to answer this is the wrong size for the question. A
recorder in the same space does it by decoding the samples and running an
FFT over them, hunting the brick-wall low-pass a lossy encoder leaves
behind, and it earns that: a spectral cut-off is the only thing that
catches a lossy file rewrapped as FLAC, where the container is lying.
That is a real feature and a much larger one — a DSP pass per file, a
confidence model, and a verdict that can be wrong. What is wanted here is
the smaller, honest half: what does the file itself claim.

TagLib# already has it. The scan opens every file to read its tags, and
the same File object carries Properties — bitrate, sample rate, bit
depth — so the answer costs one more open per row and no new package,
no ffprobe, and no decoding. AudioQuality holds those three numbers and
nothing else, and the tier falls out of them. Lossless is decided by bit
depth rather than by codec name: TagLib# populates BitsPerSample only for
codecs that have a fixed one, which is exactly the lossless set, and
reports zero for MP3, AAC, Opus and Vorbis, none of which have one. That
is a free and reliable discriminator, and it beats matching a list of
codec strings that has to stay right about formats nobody here has seen.

The reader is its own seam rather than an extra return value on
ILibraryTagStore. That interface is heavily covered and exists so a scan
can be tested without a real audio file for every case; widening Read to
return a pair would have rewritten those tests to carry a value none of
them care about. A second interface costs one more TagLib# open per file
during a scan the user asked for, on a folder measured in thousands, and
buys leaving that seam alone. LibraryTrack takes the quality as an
optional third argument for the same reason — every existing two-argument
construction, nearly all of it in tests, keeps compiling untouched.

Only the low tier is coloured. A row that announces itself in amber for
being 320 kbps is a warning about nothing, and a page where most rows are
coloured has taught the user to stop reading the colour. Lossy under
128 kbps is the one reading worth a second look, so it is the only one
that gets the caution brush; the rest is stated and left alone. The
label goes under the file name in the existing hint style rather than
into a pill beside the "Will change" badge, because it describes the file
the row came from, not something Save is going to do about it.

The .m4a label is the one place the extension cannot answer on its own —
Offstream only ever writes AAC into it, but a file from elsewhere can be
ALAC. The tier already knows, so a lossless .m4a is labelled ALAC rather
than guessed wrong.

TagLibAudioQualityReaderTests encodes with BitrateMode.Constant rather
than the averaging default, which is not incidental. The shared fixture's
source is a two-second 440 Hz sine — trivially compressible — and ABR
allocates on spectral demand, so a request for 320 kbps came back under
128 and the tier assertion failed on encoder behaviour that was entirely
correct. The plan already records the neighbouring version of this trap,
that pink noise is the wrong thing to measure ABR on. CBR pins every
frame to the request and makes the reading deterministic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@revtex
revtex deleted the branch normalize-metadata-page-eol September 4, 2026 16:08
@revtex revtex closed this Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant