Autotag: identify tracks by audio fingerprint (Chromaprint + AcoustID) #96

Open
opened 2026-08-18 20:37:45 +00:00 by yonlu · 2 comments
Owner

Identify a track by what it sounds like rather than by what its tags claim. Chromaprint generates a fingerprint, AcoustID maps that to MusicBrainz recording MBIDs, and the existing autotag machinery takes it from there.

Salvaged from section 5.2 of docs/dev/roadmap.md, which is deleted — every other part of its MusicBrainz phase has shipped.

Why it is worth having

The autotagger currently matches on metadata: normalized titles, track counts, durations. That works well on a library whose tags are approximately right and not at all on the case that most needs help — a folder of track01.mp3 with no tags at all, where there is nothing to match on. Fingerprinting is the only thing that answers those, and they are exactly the albums a user cannot fix by hand.

It also raises confidence on the ambiguous cases the scorer refuses to auto-accept: a bonus-track edition and its plain pressing have the same titles and nearly the same durations, and different recordings.

The seam already exists

#91 proposes type Identifier interface { Identify(path) ([]Candidate, error) } with MetadataIdentifier as the v1 implementation, explicitly so that an AcoustIDIdentifier is a new implementation rather than a rewrite of the call sites. This issue is the reason that seam is worth building — the argument in #91 says the interface is speculative generality unless fingerprinting is actually wanted. If this is wanted, it is not.

The cost to weigh first

Chromaprint is a C library. This project has spent real effort staying off cgo — cmd/indexbuild and cmd/indexexport build with CGO_ENABLED=0 so they need no GTK, and the SQLite driver is modernc's pure-Go one. The options are bundling the fpcalc binary and calling it (which keeps that property, at the cost of shipping a binary per platform and a packaging change in four recipes), or cgo bindings (which do not). fpcalc is what Picard itself ships.

An AcoustID API key is also required, and it is per-application.

Done when

A folder of untagged files can be identified and tagged, and the autotag confidence for an ambiguous edition improves rather than merely being another opinion.

Identify a track by what it *sounds like* rather than by what its tags claim. Chromaprint generates a fingerprint, AcoustID maps that to MusicBrainz recording MBIDs, and the existing autotag machinery takes it from there. Salvaged from section 5.2 of `docs/dev/roadmap.md`, which is deleted — every other part of its MusicBrainz phase has shipped. ## Why it is worth having The autotagger currently matches on metadata: normalized titles, track counts, durations. That works well on a library whose tags are approximately right and not at all on the case that most needs help — a folder of `track01.mp3` with no tags at all, where there is nothing to match *on*. Fingerprinting is the only thing that answers those, and they are exactly the albums a user cannot fix by hand. It also raises confidence on the ambiguous cases the scorer refuses to auto-accept: a bonus-track edition and its plain pressing have the same titles and nearly the same durations, and different recordings. ## The seam already exists #91 proposes `type Identifier interface { Identify(path) ([]Candidate, error) }` with `MetadataIdentifier` as the v1 implementation, explicitly so that an `AcoustIDIdentifier` is a new implementation rather than a rewrite of the call sites. **This issue is the reason that seam is worth building** — the argument in #91 says the interface is speculative generality unless fingerprinting is actually wanted. If this is wanted, it is not. ## The cost to weigh first Chromaprint is a C library. This project has spent real effort staying off cgo — `cmd/indexbuild` and `cmd/indexexport` build with `CGO_ENABLED=0` so they need no GTK, and the SQLite driver is modernc's pure-Go one. The options are bundling the `fpcalc` binary and calling it (which keeps that property, at the cost of shipping a binary per platform and a packaging change in four recipes), or cgo bindings (which do not). `fpcalc` is what Picard itself ships. An AcoustID API key is also required, and it is per-application. ## Done when A folder of untagged files can be identified and tagged, and the autotag confidence for an ambiguous edition improves rather than merely being another opinion.
yonlu added the Platform/DesktopKind/Feature
Priority
Low
4
Area/Metadata
labels 2026-08-18 20:37:45 +00:00
Author
Owner

Research done, with measurements rather than reading. Full plan: .planning/plans/active/022-audio-fingerprinting.md (written locally; lands with the first branch).

Short version — the two costs this issue worried about are both gone.

Chromaprint in pure Go: no cgo, no binary. alexgorbatchev/gochromaprint reproduces reference fpcalc output essentially bit-for-bit when fed PCM from our own metadata.DecodeFile: 948/948 subfingerprints identical on a FLAC, 615/616 on an MP3 (decoder rounding, not algorithm), ~500× realtime for the fingerprint stage. CGO_ENABLED=0 survives, Android included, and there is no fifth packaging recipe. One caveat: the published v1.6.0 fails go get with a sumdb checksum mismatch — the tag was rewritten after publication — so it gets vendored (MIT, zero deps, ~2k LOC + tests) behind a conformance test against system fpcalc.

It works on our own music. 19 files sampled from test_data/mb-tag/: 19/19 matched, and all 8 files carrying a MUSICBRAINZ_TRACKID came back with that exact MBID, scores 0.950–1.000. meta=recordingids releasegroups returns the recording and the release-group MBID, so candidates can be built from the local release-group cache with no MusicBrainz search call.

Three protocol facts that would each have cost a day, all measured:

  • duration must be the whole file's duration. Tolerance is ±7 s: 129→match, +5 s→match, +8 s→zero results. A 120 s fingerprint window is fine — identical results to a full-file fingerprint on a 315 s track — as long as the duration is real, which audio_files.length_milliseconds already is.
  • More than one fingerprint per request needs batch=1. Without it the server silently answers only the first query. Up to 20 per request (annotated "temporary, decrease to 10 or less later"), which makes an album one request — so the 3 req/s guideline is not the constraint it looked like.
  • meta is whitespace-separated, not +.

Decode is the only real cost: fingerprint ~500× realtime, MP3 decode ~30–55×, FLAC ~120–210× through beep. The scan never decodes today (header-only duration parsers), so this is new work — ~3 s per file with the 120 s window, so it wants to be an opt-in, pausable job plus on-demand analysis of one album, not a scan step.

What I'd build, in order:

  1. Fingerprint → AcoustID → recording MBID, as a new "fingerprint" step in the existing candidate cascade behind #91's Identifier seam, resolved through the local release-group cache. That is the track01.mp3 case, which nothing else can serve, and it can break ties where the scorer is torn.
  2. The same primitive inside the StateVerifying window (backend/download/manager.go:1124), fingerprinting staged files and checking them against dl.Expected — needs a RecordingMBID on ExpectedTrack, which explore.MBTrack.MBID already carries. Advisory only: it records and flags, never blocks an import, and it is also the full-decode integrity check the pipeline has never had.
  3. Spectral cutoff / loudness on the same decode, later and advisory only — ~97–99% on paper, but a vinyl needle-drop and a legitimately low-passed master roll off the same way, so it must never be a gate.

Two corrections to this issue's framing. The fingerprint resolves the recording, not the release: a remaster and the original pressing fingerprint the same, so it will not choose between a bonus-track edition and a plain pressing — that stays with the length/date/release-meta signals. And what it fixes outright is the untagged/mislabeled file, which is the case the scorer cannot touch at all.

Download verification and the quality diagnostics have no issue yet; say the word and I'll open them.

Research done, with measurements rather than reading. Full plan: `.planning/plans/active/022-audio-fingerprinting.md` (written locally; lands with the first branch). Short version — the two costs this issue worried about are both gone. **Chromaprint in pure Go: no cgo, no binary.** `alexgorbatchev/gochromaprint` reproduces reference `fpcalc` output essentially bit-for-bit when fed PCM from our own `metadata.DecodeFile`: 948/948 subfingerprints identical on a FLAC, 615/616 on an MP3 (decoder rounding, not algorithm), ~500× realtime for the fingerprint stage. `CGO_ENABLED=0` survives, Android included, and there is no fifth packaging recipe. One caveat: the published v1.6.0 fails `go get` with a sumdb checksum mismatch — the tag was rewritten after publication — so it gets vendored (MIT, zero deps, ~2k LOC + tests) behind a conformance test against system `fpcalc`. **It works on our own music.** 19 files sampled from `test_data/mb-tag/`: 19/19 matched, and all 8 files carrying a `MUSICBRAINZ_TRACKID` came back with that exact MBID, scores 0.950–1.000. `meta=recordingids releasegroups` returns the recording *and* the release-group MBID, so candidates can be built from the local release-group cache with no MusicBrainz search call. Three protocol facts that would each have cost a day, all measured: - `duration` must be the **whole file's** duration. Tolerance is ±7 s: 129→match, +5 s→match, +8 s→**zero results**. A 120 s fingerprint window is fine — identical results to a full-file fingerprint on a 315 s track — as long as the duration is real, which `audio_files.length_milliseconds` already is. - More than one fingerprint per request needs `batch=1`. Without it the server silently answers only the first query. Up to 20 per request (annotated "temporary, decrease to 10 or less later"), which makes an album one request — so the 3 req/s guideline is not the constraint it looked like. - `meta` is whitespace-separated, not `+`. **Decode is the only real cost**: fingerprint ~500× realtime, MP3 decode ~30–55×, FLAC ~120–210× through beep. The scan never decodes today (header-only duration parsers), so this is new work — ~3 s per file with the 120 s window, so it wants to be an opt-in, pausable job plus on-demand analysis of one album, not a scan step. What I'd build, in order: 1. Fingerprint → AcoustID → recording MBID, as a new `"fingerprint"` step in the existing candidate cascade behind #91's `Identifier` seam, resolved through the local release-group cache. That is the `track01.mp3` case, which nothing else can serve, and it can break ties where the scorer is torn. 2. The same primitive inside the `StateVerifying` window (`backend/download/manager.go:1124`), fingerprinting staged files and checking them against `dl.Expected` — needs a `RecordingMBID` on `ExpectedTrack`, which `explore.MBTrack.MBID` already carries. Advisory only: it records and flags, never blocks an import, and it is also the full-decode integrity check the pipeline has never had. 3. Spectral cutoff / loudness on the same decode, later and advisory only — ~97–99% on paper, but a vinyl needle-drop and a legitimately low-passed master roll off the same way, so it must never be a gate. Two corrections to this issue's framing. The fingerprint resolves the *recording*, not the release: a remaster and the original pressing fingerprint the same, so it will not choose between a bonus-track edition and a plain pressing — that stays with the length/date/release-meta signals. And what it fixes outright is the untagged/mislabeled file, which is the case the scorer cannot touch at all. Download verification and the quality diagnostics have no issue yet; say the word and I'll open them.
yonlu added a new dependency 2026-09-27 03:01:34 +00:00
yonlu added the Platform/Android label 2026-09-27 03:01:35 +00:00
yonlu self-assigned this 2026-09-27 03:03:31 +00:00
yonlu added the
Status
In Progress
label 2026-09-27 03:03:32 +00:00
Author
Owner

Claiming. Branch feat/96-audio-fingerprint (Phase 2, #277, will land on the same batch branch).

Plan 022 was checked against the code before starting and revised — see its new "Revisions to the first draft" section. The substantive changes:

  • no sql/migrations/ (schema is one description; new tables only);
  • no local release_groups cache exists, so the candidate comes from AcoustID's nested release ids (meta=recordingids releasegroupids releaseids), voted locally → at most two MB lookups per group;
  • a fingerprint never writes audio_files.recording_mbid (tag-projected, Owned); it is stored evidence, and the MBID reaches the file only via Apply;
  • the fingerprint itself is not stored (~165 MB/50k tracks, nothing reads it) — only the answer (acoustid_matches, Cache + Cascade);
  • IDMatch alone cannot separate editions with identical titles, so the scorer gains an identity/contradiction term active only when evidence exists, weighted from the eval harness (which grows MBID fields).

Also opened #277 (download verification) and #278 (quality diagnostics), both depending on this.

Needs from a maintainer: an AcoustID application key registered at acoustid.org/new-application. Until it exists the feature reports "no key" and sends nothing; everything is tested against a stub server.

Claiming. Branch `feat/96-audio-fingerprint` (Phase 2, #277, will land on the same batch branch). Plan 022 was checked against the code before starting and revised — see its new "Revisions to the first draft" section. The substantive changes: - no `sql/migrations/` (schema is one description; new tables only); - no local `release_groups` cache exists, so the candidate comes from AcoustID's nested release ids (`meta=recordingids releasegroupids releaseids`), voted locally → at most two MB lookups per group; - a fingerprint never writes `audio_files.recording_mbid` (tag-projected, Owned); it is stored evidence, and the MBID reaches the file only via Apply; - the fingerprint itself is not stored (~165 MB/50k tracks, nothing reads it) — only the answer (`acoustid_matches`, Cache + Cascade); - IDMatch alone cannot separate editions with identical titles, so the scorer gains an identity/contradiction term active only when evidence exists, weighted from the eval harness (which grows MBID fields). Also opened #277 (download verification) and #278 (quality diagnostics), both depending on this. Needs from a maintainer: an AcoustID **application** key registered at acoustid.org/new-application. Until it exists the feature reports "no key" and sends nothing; everything is tested against a stub server.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: yonlu/yellowjacket#96