Phase: 2-Extended (builds on Phase 2A)
Dependencies: T-400 (AudioVariant model), T-401 (CanonicalStats)
Branch:experimental/brainz
Estimated Duration: 4-6 weeks
Tasks: T-420 through T-430
This phase extends the basic AudioVariant model (from T-400) with codec-specific fingerprints and sophisticated quality heuristics for FLAC, MP3, Opus, and AAC.
- Provide stable identities for audio variants independent of tags/metadata.
- Detect obvious and likely transcodes with codec-specific logic.
- Rank variants by effective quality in a way that is good enough for:
- Canonical edition selection.
- Multi-swarm scheduling.
- Collection Doctor / repair suggestions.
This is explicitly heuristic and versioned; we store raw features so we can recompute scores when heuristics change.
mb_release_id(nullable; MB Release ID, string)file_path(string) - local path for library scansfile_hash_sha256(binary/hex string; whole-file hash)
acoustid_id(nullable string) – AcoustID / Chromaprint ID, if resolvedchromaprint_fp(nullable blob) – optional raw fingerprint for local-only useaudio_sketch_hash(nullable; short codec-agnostic PCM sketch hash, e.g. 64–128 bits)- Derived by:
- Decoding 2–3 short PCM windows (start/mid/end).
- Downsampling heavily (e.g. mono 4 kHz).
- Hashing resulting PCM.
- Derived by:
quality_score(f32, 0.0–1.0)transcode_suspect(bool)analyzer_version(string, e.g.audioqa-1)
These are produced by codec-specific analyzers plus generic logic. Raw features used to compute them are stored in codec-specific sections (below).
Important:
quality_scoreis relative and pragmatic, not audiophile-perfect. Its purpose is to rank candidates, not to be a scientific metric.
FLAC is lossless and has especially useful header metadata. We exploit that.
-- Add to HashDb migration (Phase 2-Extended)
ALTER TABLE HashDb ADD COLUMN flac_streaminfo_hash42 TEXT;
ALTER TABLE HashDb ADD COLUMN flac_pcm_md5 TEXT;
ALTER TABLE HashDb ADD COLUMN flac_min_block_size INTEGER;
ALTER TABLE HashDb ADD COLUMN flac_max_block_size INTEGER;
ALTER TABLE HashDb ADD COLUMN flac_min_frame_size INTEGER;
ALTER TABLE HashDb ADD COLUMN flac_max_frame_size INTEGER;
ALTER TABLE HashDb ADD COLUMN flac_total_samples INTEGER;
CREATE INDEX idx_hashdb_flac_streaminfo ON HashDb(flac_streaminfo_hash42);
CREATE INDEX idx_hashdb_flac_pcm ON HashDb(flac_pcm_md5);-
flac_streaminfo_hash42(nullable, 64–128 bits)- Hash (e.g. xxHash64) of the first 42 bytes:
fLaCmagic (4)- STREAMINFO header (4)
- STREAMINFO body (34)
- Purpose: fast identity of the audio stream ignoring tags/artwork.
- Hash (e.g. xxHash64) of the first 42 bytes:
-
flac_pcm_md5(nullable, 16 bytes)- The MD5 of decoded PCM stored in STREAMINFO.
- Strong content equality indicator across FLAC files.
-
Streaminfo-derived fields (some may duplicate core fields but are cheap to validate):
flac_min_block_sizeflac_max_block_sizeflac_min_frame_sizeflac_max_frame_sizeflac_total_samples(u64)
These are primarily for integrity checks and internal sanity verification.
Because FLAC is lossless, quality is dominated by:
- Is it truly lossless source material?
- Is the sample rate / bit-depth sane for the recording?
- Is it a transcode from a lossy source saved as FLAC?
-
Start from:
- 0.95 for normal "CD-quality" FLAC:
44.1 or 48 kHz,16 or 24 bit. - 1.00 for high-res FLAC with evidence of real content above ~20 kHz and non-suspicious provenance.
- 0.95 for normal "CD-quality" FLAC:
-
Slight penalties:
- Downsampled weirdness (e.g. 32 kHz when MB metadata or other variants suggest 44.1 kHz): −0.05 to −0.10.
- Unusual bit depths that don't match MB/other variants (e.g. 20-bit with nothing significant in the extra bits): −0.02 to −0.05.
We set transcode_suspect = true for FLAC when evidence suggests "lossy-sourced FLAC":
-
Spectral analysis on short PCM windows:
- Detect classic brickwall lowpass patterns:
- Tight lowpass at ~16 kHz or ~18 kHz with no energy beyond, despite 44.1/48 kHz sample rate.
- Consistency with typical MP3/AAC encoder profiles.
- Detect classic brickwall lowpass patterns:
-
If:
flac_pcm_md5matches the PCM of a known lossy variant (via decode) ORaudio_sketch_hashmatches a lossy file and the spectral shape matches the lossy variant,- Then mark as transcode-suspect.
-
If
transcode_suspect == true:- Hard cap:
quality_score <= 0.6. - Typically:
- Start from hypothetical FLAC base (0.95 / 1.0).
- Apply penalty e.g.
score *= 0.5(so 0.95 → 0.47).
- Hard cap:
-
If not suspect:
- Score stays at high end (0.9–1.0).
MP3 is lossy with a more complex container/bitstream situation. We focus on stream identity and spectral behaviour.
ALTER TABLE HashDb ADD COLUMN mp3_stream_hash TEXT;
ALTER TABLE HashDb ADD COLUMN mp3_encoder TEXT;
ALTER TABLE HashDb ADD COLUMN mp3_encoder_preset TEXT;
ALTER TABLE HashDb ADD COLUMN mp3_frames_analyzed INTEGER;
ALTER TABLE HashDb ADD COLUMN effective_bandwidth_hz REAL;
ALTER TABLE HashDb ADD COLUMN nominal_lowpass_hz REAL;
ALTER TABLE HashDb ADD COLUMN spectral_flatness_score REAL;
ALTER TABLE HashDb ADD COLUMN hf_energy_ratio REAL;
CREATE INDEX idx_hashdb_mp3_stream ON HashDb(mp3_stream_hash);
CREATE INDEX idx_hashdb_encoder ON HashDb(mp3_encoder);-
mp3_stream_hash(nullable, 64–128 bits)- Hash over a tag-stripped version of the file:
- Remove ID3v2 from start and ID3v1/APEv2 from end.
- Hash a fixed number of frames or the entire frame sequence.
- Purpose: identify identical encodes regardless of tags.
- Hash over a tag-stripped version of the file:
-
mp3_encoder(string, e.g.LAME,GOGO,FhG,Unknown) -
mp3_encoder_preset(string/enum when LAME/Xing present, e.g.CBR_320,V0,V2, etc.) -
mp3_frames_analyzed(u32)- Number of frames sampled for features.
Spectral/quality features:
-
effective_bandwidth_hz(f32)- Approximate upper frequency with non-trivial energy (from short-time FFT).
-
nominal_lowpass_hz(nullable f32; from encoder tag if exposed). -
spectral_flatness_score/hf_energy_ratio(optional floats)- Simple stats for how "alive" the HF region is vs noise/aliasing.
We combine:
- Nominal bitrate/preset
- Effective bandwidth
- Encoder reputation
Pseudo approach:
-
Base score from nominal bitrate/preset (for music content):
CBR_320orV0→ ~0.80V2(approx 190 kbps) → ~0.70~160 kbps→ ~0.60128 kbps→ ~0.50<128 kbps→ 0.3 or lower
-
Adjust for effective bandwidth:
-
If bitrate suggests high bandwidth (e.g. 320k) but
effective_bandwidth_hz < 16 kHz:- Apply penalty, e.g.
score *= 0.7.
- Apply penalty, e.g.
-
If bitrate is modest but spectrum looks "better than expected":
- Small bump, e.g.
score *= 1.05(capped at some max).
- Small bump, e.g.
-
-
Adjust for encoder:
- Known good encoders (LAME recent) → no change or slight bump.
- Unknown/old encoders → slight penalty (−0.05 to −0.1).
Final score: clamped to [0.0, 0.85] to keep lossless/Hi-Res FLAC above.
Mark transcode_suspect = true if any of:
-
Bitrate inconsistent with spectrum:
- e.g.
CBR_320buteffective_bandwidth_hzlooks like a typical 128k (sharp lowpass around ~16 kHz, little HF detail).
- e.g.
-
audio_sketch_hashmatches a lower bitrate MP3/Opus/AAC variant closely:- And the lower bitrate file existed first or is more widely seen.
-
Encoder tag anomalies:
- LAME header or other metadata declares one preset, but file metrics match another, suggesting re-encode.
Effect on score:
- Transcode-suspect:
- Force
quality_score <= 0.5. - Typically reduce by 0.2–0.3 vs non-suspect of same nominal bitrate.
- Force
Opus is modern and tends to be efficient at lower bitrates.
ALTER TABLE HashDb ADD COLUMN opus_stream_hash TEXT;
ALTER TABLE HashDb ADD COLUMN opus_nominal_bitrate_kbps INTEGER;
ALTER TABLE HashDb ADD COLUMN opus_application TEXT;
ALTER TABLE HashDb ADD COLUMN opus_bandwidth_mode TEXT;
CREATE INDEX idx_hashdb_opus_stream ON HashDb(opus_stream_hash);-
opus_stream_hash(nullable, 64–128 bits)- Hash of:
- Ogg Opus ID header page + first N audio data pages.
- Identity for the audio stream ignoring container-level metadata.
- Hash of:
-
opus_nominal_bitrate_kbps(nullable u32; from container or encoder tags) -
opus_application(enum:VoIP,Audio,LowDelay) -
opus_bandwidth_mode(enum:Narrowband,Mediumband,Wideband,Superwideband,Fullband)
Spectral features (reuse from MP3):
effective_bandwidth_hz(f32)hf_energy_ratio(float) – similar to MP3, but tuned for Opus's typical spectral behavior.
For music content (our default assumption unless we know otherwise):
-
Base score from nominal bitrate:
>= 160 kbps→ ~0.80128–160 kbps→ ~0.7596–128 kbps→ ~0.7064–96 kbps→ ~0.60<64 kbps→ 0.4 or below for music.
-
Adjust for bandwidth mode:
-
If
Fullbandwith goodeffective_bandwidth_hz(≥ 18–20 kHz):- Slight bump, e.g. +0.05 (capped at 0.85).
-
If mode is
Narrowband/Widebandbut MB or other variants suggest fullband music:- Penalty (−0.1 or more).
-
-
Apply small bonuses for:
- Matching expected spectral shape for Opus at that bitrate.
- Modern encoder builds (if detectable).
Clamp:
quality_scorefor Opus typically in [0.4, 0.85].- Lossless still has headroom above it.
Mark transcode_suspect = true if:
-
Opus file appears to be re-encoded from:
- Another lossy variant with similar
audio_sketch_hashbut lower base quality.
- Another lossy variant with similar
-
Effective bandwidth is strangely low for the advertised bitrate and bandwidth mode, and spectrum looks like a known MP3/AAC artifact profile.
Effect on score:
- Similar to MP3:
- Multiply by ~0.6–0.7 and cap at ~0.5.
Includes AAC-LC and HE-AAC variants, typically stored in MP4/M4A or raw ADTS.
ALTER TABLE HashDb ADD COLUMN aac_stream_hash TEXT;
ALTER TABLE HashDb ADD COLUMN aac_profile TEXT;
ALTER TABLE HashDb ADD COLUMN aac_sbr_present BOOLEAN;
ALTER TABLE HashDb ADD COLUMN aac_ps_present BOOLEAN;
ALTER TABLE HashDb ADD COLUMN aac_nominal_bitrate_kbps INTEGER;
CREATE INDEX idx_hashdb_aac_stream ON HashDb(aac_stream_hash);
CREATE INDEX idx_hashdb_aac_profile ON HashDb(aac_profile);-
aac_stream_hash(nullable, 64–128 bits)- Hash over:
- Raw AAC frames (for ADTS) or
mdataudio payload segments (for MP4), ignoring metadata atoms.
- Raw AAC frames (for ADTS) or
- Hash over:
-
aac_profile(enum:LC,HE,HEv2,LD,ELD, etc.) -
aac_sbr_present(bool) -
aac_ps_present(bool) – parametric stereo -
aac_nominal_bitrate_kbps(nullable u32)
Spectral features (same as others):
effective_bandwidth_hz(f32)hf_energy_ratio(float)
For music:
-
Base score from profile + nominal bitrate:
-
AAC-LC:
>= 256 kbps→ ~0.80192–256 kbps→ ~0.75128–192 kbps→ ~0.7096–128 kbps→ ~0.60<96 kbps→ 0.4 or below.
-
HE-AAC / HE-AACv2:
64–96 kbps(when used appropriately) → ~0.6548–64 kbps→ ~0.55<48 kbps→ 0.4 or below.
-
-
Adjust for effective bandwidth:
-
LC at 256 kbps should show robust HF content:
- If
effective_bandwidth_hz < 16 kHz, apply penalty (* 0.7).
- If
-
HE profiles at lower bitrates:
- Expect a different spectral shape; penalize if it looks like a downsampled MP3 transcode.
-
Clamp scores around [0.4, 0.85], similar to Opus.
Set transcode_suspect = true if:
-
Evidence suggests:
- Re-encode of MP3 or another AAC:
audio_sketch_hashmatches lower-quality file.- Spectral content matches typical MP3 lowpass while container says AAC-LC at high bitrate.
- Re-encode of MP3 or another AAC:
-
Container bitrate/profile mismatch:
- Declared 256 kbps LC but actual payload characteristics + spectrum look like 96 kbps HE.
Apply penalties and cap quality similar to MP3/Opus.
The following fields are used to correlate variants across codecs and containers:
mb_recording_id,mb_release_idacoustid_idaudio_sketch_hash(PCM-window hash)- Codec-specific stream hashes:
flac_streaminfo_hash42mp3_stream_hashopus_stream_hashaac_stream_hash
Rules of thumb:
- If two variants share:
- The same MB Recording ID and
audio_sketch_hashand- Similar duration within a small tolerance, then they are considered different encodes/containers of the same logical content.
Given a set of AudioVariants for a single MB Recording:
-
Prefer in this order (simplified):
- Non-transcode, lossless variants (FLAC/ALAC/WAV), highest
quality_score. - Non-transcode, high-quality Opus/AAC/MP3 (score-driven).
- Transcode-suspect variants only if nothing better exists.
- Non-transcode, lossless variants (FLAC/ALAC/WAV), highest
-
Use
quality_scoreto differentiate within categories. -
Use codec-specific stream hashes to deduplicate identical copies.
For each new or modified file:
- Parse container and codec.
- Compute:
- Common fields (duration, sample rate, bitrate, etc.).
- Codec-specific fingerprints and features.
- Acoustic fingerprint (if enabled).
- Pass feature bundle to codec-specific analyzer:
- Returns:
quality_scoretranscode_suspect
- Returns:
- Store:
- Raw features (for recomputation later).
- Final
quality_score,transcode_suspect. analyzer_version.
-
When heuristics change (new version of analyzers):
- Bump
analyzer_version. - Background job:
- Scans existing
AudioVariantrecords with older version. - Recomputes scores/flags using stored raw features.
- Scans existing
- Bump
-
This avoids re-decoding audio when possible.
AudioAnalyzerService (coordinator)
├── FLACAnalyzer
│ ├── ExtractStreamInfo()
│ ├── ComputeQualityScore()
│ └── DetectTranscode()
├── MP3Analyzer
│ ├── ExtractStreamHash()
│ ├── DetectEncoder()
│ ├── AnalyzeSpectrum()
│ └── ComputeQualityScore()
├── OpusAnalyzer
│ └── ...
└── AACAnalyzer
└── ...
Each analyzer is registered in DI and invoked by AudioAnalyzerService based on codec detection.
Deliverables:
- Add all codec-specific fields to
AudioVariant.cs - HashDb migration (version 7) for new columns + indexes
- Update
HashDbServiceto persist all new fields
Files:
src/slskd/Audio/AudioVariant.cssrc/slskd/HashDb/Migrations/HashDbMigrations.cssrc/slskd/HashDb/HashDbService.cs
Deliverables:
FLACAnalyzerclass with:- STREAMINFO parser (extract 42-byte hash, PCM MD5, block/frame sizes)
- Quality scoring heuristics
- Transcode detection via spectral analysis
- Integration with
AudioAnalyzerService
Files:
src/slskd/Audio/Analyzers/FLACAnalyzer.cssrc/slskd/Audio/AudioAnalyzerService.cs
Deliverables:
MP3Analyzerclass with:- Tag-stripped stream hash
- Encoder detection (LAME, etc.)
- Spectral feature extraction (bandwidth, flatness, HF energy)
- Quality scoring with encoder/bitrate/spectrum logic
- Transcode detection
Files:
src/slskd/Audio/Analyzers/MP3Analyzer.cs
Deliverables:
OpusAnalyzerclass with:- Ogg Opus stream hash
- Bitrate/application/bandwidth mode extraction
- Quality scoring tuned for Opus
- Transcode detection
Files:
src/slskd/Audio/Analyzers/OpusAnalyzer.cs
Deliverables:
AACAnalyzerclass with:- AAC stream hash (MP4/ADTS)
- Profile detection (LC/HE/HEv2), SBR/PS flags
- Quality scoring for AAC variants
- Transcode detection
Files:
src/slskd/Audio/Analyzers/AACAnalyzer.cs
Deliverables:
- Service to decode short PCM windows from arbitrary audio files
- Downsample to mono 4 kHz
- Hash resulting PCM (xxHash64 or similar)
- Store in
audio_sketch_hashfield
Files:
src/slskd/Audio/AudioSketchService.cs
Dependencies:
- Requires ffmpeg for decoding (already used in Phase 1B)
Deliverables:
- Service to query variants by:
- MB Recording ID + audio_sketch_hash
- Codec-specific stream hashes
- Deduplicate identical variants across codec boundaries
- API endpoint for debugging:
GET /api/audio/variants/dedupe/{recordingId}
Files:
src/slskd/Audio/VariantDeduplicationService.cssrc/slskd/Audio/API/VariantDedupeController.cs
Deliverables:
- Background job to detect stale
analyzer_version— Done:AnalyzerMigrationService.MigrateAsync, uses QualityScorer + TranscodeDetector on stored variant fields. - Recompute quality scores from stored raw features — Done.
- CLI:
slskdn audio reanalyze [--force]— Done:--audio-reanalyzeruns migration at startup;--audio-reanalyze-forcerecomputes all. API:POST /api/audio/analyzers/migrate?targetVersion=audioqa-1&force=true.
Files:
src/slskd/Audio/AnalyzerMigrationService.cs(existing; addedforceparam)src/slskd/Audio/API/AnalyzerMigrationController.cs(existing; addedforcequery)src/slskd/Core/Options.cs(Flags:AudioReanalyze,AudioReanalyzeForce)src/slskd/Program.cs(startup hook when--audio-reanalyze)
Deliverables:
- Modify
CanonicalStatsServiceto:- Use codec-specific stream hashes for deduplication
- Prefer lossless over lossy explicitly
- Use audio_sketch_hash for cross-codec grouping
Files:
src/slskd/Audio/CanonicalStatsService.cs(update existing from T-401)
Deliverables:
- Extend
LibraryHealthServiceto:- Detect codec mismatches (e.g., "FLAC but spectral content looks like MP3")
- Flag transcodes using new analyzer results
- Suggest replacements based on codec-specific canonical variants
Files:
src/slskd/LibraryHealth/LibraryHealthService.cs(update existing from T-403)
Deliverables:
- Test fixtures: sample FLAC, MP3, Opus, AAC files (small, deterministic)
- Unit tests for each analyzer:
- Quality score computation
- Transcode detection
- Stream hash stability
- Integration tests for cross-codec deduplication
Files:
tests/slskd.Tests.Unit/Audio/FLACAnalyzerTests.cstests/slskd.Tests.Unit/Audio/MP3AnalyzerTests.cstests/slskd.Tests.Unit/Audio/OpusAnalyzerTests.cstests/slskd.Tests.Unit/Audio/AACAnalyzerTests.cstests/slskd.Tests.Unit/Audio/CrossCodecDeduplicationTests.cs
-
All formats share a common AudioVariant core and codec-agnostic fingerprints (
audio_sketch_hash, AcoustID). -
Each codec adds:
- A stream-identity hash that ignores tags.
- Optional encoder/bitstream details.
- Spectral and encoder-derived features.
-
quality_scoreandtranscode_suspectare computed via codec-specific analyzers, but normalized so:- Lossless always has headroom over lossy.
- High-quality modern lossy (Opus/AAC/MP3 320) is clearly distinguishable from low-bitrate or transcode garbage.
-
The multi-swarm engine and Collection Doctor consume just:
- MBIDs,
quality_score,transcode_suspect,- and identity hashes, without knowing the heuristic details.
This keeps the architecture codec-agnostic, while allowing codec-specific magic (like FLAC's 42-byte trick and MP3/Opus transcode detection) to be added and refined over time.
Phase 2-Extended specification complete. Ready for implementation.