Skip to content

feat(fingerprint): write ACOUSTID_FINGERPRINT to file tags and skip Chromaprint when fingerprint already known#2052

Open
VictorVow wants to merge 2 commits intostrawberrymusicplayer:masterfrom
VictorVow:feat/write-fingerprint-tags
Open

feat(fingerprint): write ACOUSTID_FINGERPRINT to file tags and skip Chromaprint when fingerprint already known#2052
VictorVow wants to merge 2 commits intostrawberrymusicplayer:masterfrom
VictorVow:feat/write-fingerprint-tags

Conversation

@VictorVow
Copy link
Copy Markdown
Contributor

Summary

Strawberry already computes Chromaprint fingerprints and stores them in the fingerprint DB column (when "Song fingerprinting and tracking" is enabled). It also reads ACOUSTID_FINGERPRINT from file tags (written by fpcalc, MusicBrainz Picard, beets, etc.) into a separate acoustid_fingerprint DB column. However, the two columns were independent and neither flow wrote back to the file or checked the other before running Chromaprint.

This PR closes those gaps:

  • New opt-in setting — "Write fingerprint to file tags (ACOUSTID_FINGERPRINT)": when enabled alongside song tracking, the ACOUSTID_FINGERPRINT tag is written to audio files after fingerprint computation during a collection scan, guarded by an idempotent check (only writes if the tag is absent) and mtime refresh to prevent false change detection on the next scan.

  • Populate acoustid_fingerprint in DB from collection scan: previously this column was only filled when reading pre-tagged files; now it is also set from the internally computed fingerprint.

  • Skip Chromaprint when fingerprint already known: TagFetcher::GetFingerprint returns early if either fingerprint or acoustid_fingerprint is non-empty. The have_fingerprints fast-path in StartFetch also checks both columns. For the "Complete tags automatically" right-click flow, fingerprint fields are refreshed from the collection DB before calling StartFetch, so a song whose in-memory playlist item was populated before the fingerprint was computed still benefits from the optimisation.

  • Write fingerprint during "Complete tags automatically": FingerprintFound stores the computed fingerprint back to songs_[index], and TrackSelectionDialog propagates it to data_[row].original_song_, so WriteFileBlocking on accept includes the ACOUSTID_FINGERPRINT tag.

  • Tag write supportACOUSTID_FINGERPRINT writes added to all five tag systems (ID3v2 TXXX, Vorbis comments, APE, MP4, ASF), guarded by !song.acoustid_fingerprint().isEmpty().

  • Error loggingWriteFileBlocking return values are now checked; failures are logged with qLog(Warning) and mtime is only refreshed on success.

Files changed

File Change
src/constants/collectionsettings.h Add kWriteFingerprintToFileTags constant
src/settings/collectionsettingspage.ui Add "Write fingerprint to file tags" checkbox (disabled unless song tracking is on)
src/settings/collectionsettingspage.cpp Load/save/toggle new setting; hide under HAVE_SONGFINGERPRINTING
src/collection/collectionwatcher.h Add write_fingerprint_to_file_tags_ member
src/collection/collectionwatcher.cpp Load setting; populate acoustid_fingerprint; optional file write with error logging
src/tagreader/tagreadertaglib.cpp Write ACOUSTID_FINGERPRINT in all five tag-write functions
src/musicbrainz/tagfetcher.cpp Skip Chromaprint when either fingerprint column is set; back-fill acoustid_fingerprint
src/dialogs/trackselectiondialog.cpp Propagate computed fingerprint to original_song_ for file write on accept
src/core/mainwindow.cpp Refresh fingerprint from DB before StartFetch for "Complete tags automatically"

Test plan

  • Enable "Song fingerprinting and tracking" + "Write fingerprint to file tags" → rescan a file → confirm ACOUSTID_FINGERPRINT appears in file tags (ffprobe or kid3)
  • Query DB: both fingerprint and acoustid_fingerprint columns populated after scan
  • Right-click → "Complete tags automatically" on a previously scanned song → confirm no Chromaprinter … Decode time: log line (Chromaprint skipped)
  • "Complete tags automatically" on a song with no prior fingerprint → fingerprint written to file on accept
  • Add a file pre-tagged with ACOUSTID_FINGERPRINT by fpcalc → scan → confirm fingerprint column populated from file tag (no Chromaprint needed)
  • Toggling "Song fingerprinting and tracking" off disables and unchecks "Write fingerprint to file tags"

**Write fingerprint during collection scan (opt-in)**
- Add "Write fingerprint to file tags" checkbox to Collection settings,
  enabled only when "Song fingerprinting and tracking" is active
- After Chromaprint computes a fingerprint, populate `acoustid_fingerprint`
  in the database and (if the setting is on) write `ACOUSTID_FINGERPRINT`
  to the audio file's own tags
- Skip the file write if the file already has the tag (idempotent)
- Refresh `mtime` in the DB after the write to prevent the next scan from
  treating the file as changed
- Cross-populate: when the DB has `acoustid_fingerprint` but no `fingerprint`,
  skip Chromaprint and reuse the existing file-tag value instead

**Write fingerprint during "Complete tags automatically"**
- `TagFetcher::FingerprintFound` stores the computed fingerprint back to
  `songs_[index].set_acoustid_fingerprint()` so it is available when
  `ResultAvailable` is emitted
- `TrackSelectionDialog::FetchTagFinished` propagates the fingerprint to
  `original_song_` so it is included in the `WriteFileBlocking` call on accept

**Tag writer: add ACOUSTID_FINGERPRINT to all five tag systems**
- ID3v2 (MP3/WAV/AIFF): TXXX "Acoustid Fingerprint" via existing `SetUserTextFrame`
- Vorbis comments (FLAC/Ogg/Opus): ACOUSTID_FINGERPRINT field
- APE (APE/WavPack/Musepack): ACOUSTID_FINGERPRINT item
- MP4 (M4A/AAC): `----:com.apple.iTunes:Acoustid Fingerprint` freeform atom
- ASF (WMA): `Acoustid/Fingerprint` attribute
All writes are guarded by `!song.acoustid_fingerprint().isEmpty()` to avoid
clobbering existing tags with empty values.

**TagFetcher optimisation**
- Skip Chromaprint when either `fingerprint` or `acoustid_fingerprint` is
  already set (`GetFingerprint` static helper and `have_fingerprints` check)
- In the "already have fingerprints" path, fall back to `acoustid_fingerprint`
  if `fingerprint` is empty, and back-fill `acoustid_fingerprint` in `songs_`
  so it flows through to the file write
Two fixes for the ACOUSTID_FINGERPRINT feature:

1. collectionwatcher: check WriteFileBlocking return value and log a
   warning on failure; only refresh mtime on success. Previously the
   result was silently discarded.

2. mainwindow: before calling TagFetcher::StartFetch for "Complete tags
   automatically", refresh fingerprint and acoustid_fingerprint fields
   from the collection DB for any song whose in-memory playlist item was
   populated before the fingerprint was computed. This allows
   TagFetcher::GetFingerprint to short-circuit and skip Chromaprint
   re-computation for songs that already have a stored fingerprint.
@VictorVow VictorVow force-pushed the feat/write-fingerprint-tags branch from e5d549e to 14587b0 Compare April 3, 2026 00:13
@jonaski
Copy link
Copy Markdown
Member

jonaski commented Apr 6, 2026

This is a nice idea, but at least as of now it won't work in practice since Strawberry's Chromaprint produces different fingerprints than ie.: Picard.
If you start checking the ACOUSTID_FINGERPRINT tag against the fingerprint field in the database it won't match, so that will actually break the song fingerprinting feature.
If we start writing the Chromaprint to the ACOUSTID_FINGERPRINT it needs to be calculated the same way that MusicBrainz Picard does it.

@VictorVow
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback. Digging into this further, there are two distinct sources of divergence:

Duration: Strawberry's Chromaprinter analyses only the first 30 seconds (kPlayLengthSecs = 30 in src/engine/chromaprinter.cpp), while fpcalc/Picard defaults to 120 seconds. This alone means the encoded fingerprint strings differ in length regardless of format or decoder. Raising kPlayLengthSecs to 120 would make Strawberry's output byte-compatible with fpcalc-written tags for lossless files.

Decoder PCM (lossy only): GStreamer's decodebin (mad/mpg123 for MP3, etc.) and FFmpeg can produce marginally different floating-point PCM for lossy formats, causing a few bit differences in Chromaprint output even for the same audio window. For lossless formats (FLAC, WAV, AIFF) the PCM is decoder-independent, so this doesn't apply.

So fixing the duration is sufficient to make cross-population safe for lossless. For lossy, the skip optimisation should only fire on fingerprint being non-empty - not on acoustid_fingerprint - so that fingerprint is always computed for song tracking regardless of whether the file was pre-tagged by Picard.

The tag write infrastructure, error logging, and the write-on-accept path for "Complete tags automatically" are all independent of this and stand on their own. Happy to revise the PR with these constraints if the direction looks right to you.

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.

2 participants