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
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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user