feat(library): measure every track's loudness in the background (M464 #4995)
release / go (push) Failing after 1m18s
release / web (push) Successful in 1m27s
release / govulncheck (push) Successful in 35s
release / integration (push) Failing after 4m13s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 5m47s
release / Build signed APK (releases and dev) (push) Canceled after 4m21s

The first step of loudness normalization: the server measures each track
with ffmpeg's EBU R128 filter (true peak, mono as dual mono) and stores the
integrated loudness, true peak and loudness range in track_loudness
(migration 0065).

It also keeps a histogram of the 400 ms gating blocks at 0.1 LU, so album
loudness can be computed exactly later with no second decode (#4996). The
histogram reproduces ffmpeg's own figure (-10.68 against -10.7 on the
captured fixture), and the analyzer logs a warning if the two ever drift.

- A background worker, cloned from the fingerprint backfill, measures every
  track, new ones included. Measuring inline in the scan was dropped: the
  analysis decodes the whole file, and a large import could pass the scan's
  one-hour stuck threshold. The scan only deletes a changed file's
  measurement; the worker ticks every 10 minutes.
- Timeouts, the cancel/missing-binary split and settled verdicts follow the
  fingerprint runner. Silence and undecodable files are stored as verdicts;
  stalls are retried. The deadline scales with track length.
- loudness_settings (enabled, files at once) and an admin card with the
  coverage gauge, under GET/PUT /api/admin/library/loudness-settings and
  GET /api/admin/library/loudness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 13:01:01 -04:00
co-authored by Claude Opus 5.5
parent edd9a3a6db
commit f3196b3443
22 changed files with 2139 additions and 3 deletions
+32
View File
@@ -364,6 +364,38 @@ export async function updateFingerprintSettings(
return api.put<FingerprintSettings>('/api/admin/library/fingerprint-settings', s);
}
// Loudness analysis (#4995) -------------------------------------------------
export type LoudnessCoverage = {
total: number;
measured: number;
// Read fine, but no part of the track was loud enough to measure: silence,
// or a file shorter than half a second.
silent: number;
unreadable: number;
pending: number;
// False when the operator has switched analysis off: pending then never
// shrinks, and nothing should read as progress.
enabled: boolean;
};
export async function getLoudnessCoverage(): Promise<LoudnessCoverage> {
return api.get<LoudnessCoverage>('/api/admin/library/loudness');
}
export type LoudnessSettings = {
enabled: boolean;
backfill_concurrency: number;
};
export async function getLoudnessSettings(): Promise<LoudnessSettings> {
return api.get<LoudnessSettings>('/api/admin/library/loudness-settings');
}
export async function updateLoudnessSettings(s: LoudnessSettings): Promise<LoudnessSettings> {
return api.put<LoudnessSettings>('/api/admin/library/loudness-settings', s);
}
// Cover-art providers ------------------------------------------------------
export type CoverProviderCapability = 'album_cover' | 'artist_thumb' | 'artist_fanart';