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>
66 lines
3.5 KiB
SQL
66 lines
3.5 KiB
SQL
-- 0065_track_loudness.up.sql — measured loudness per track, for loudness
|
|
-- normalization (Scribe milestone #464, #4995).
|
|
--
|
|
-- Measured with ffmpeg's EBU R128 filter rather than read from ReplayGain tags:
|
|
-- tags in the wild are written against four different reference levels, and
|
|
-- most files have none. internal/library/loudness.go says how it is measured.
|
|
--
|
|
-- A table of its own rather than columns on tracks, for the reason
|
|
-- track_fingerprints is (0058): tracks is read with SELECT * on the hot path,
|
|
-- and the block histogram is a few hundred integers only album loudness reads.
|
|
--
|
|
-- What a row means, which the backfill depends on:
|
|
-- no row never analyzed, or the file changed since
|
|
-- analysis_version < current measured by an older method; measure again
|
|
-- analysis_version = current settled until the file changes:
|
|
-- integrated_lufs NOT NULL measured
|
|
-- integrated_lufs NULL, unreadable false
|
|
-- read fine, but no 400 ms block was above
|
|
-- the -70 LUFS gate: silence, or too short
|
|
-- unreadable true ffmpeg could not decode the file
|
|
-- A failure that says nothing about the file (a timeout, a cancelled pass, a
|
|
-- missing ffmpeg) writes no row, so the backfill tries again.
|
|
CREATE TABLE track_loudness (
|
|
track_id uuid PRIMARY KEY REFERENCES tracks (id) ON DELETE CASCADE,
|
|
-- Gated integrated loudness (ITU-R BS.1770), mono measured as dual mono.
|
|
integrated_lufs real,
|
|
-- Highest inter-sample peak, from 4x oversampling, in dB relative to full
|
|
-- scale. NULL for digital silence, whose peak is -inf.
|
|
true_peak_dbtp real,
|
|
-- Loudness range (EBU Tech 3342): how much the loudness moves within the
|
|
-- track. Not used for gain; kept because it costs nothing here.
|
|
loudness_range_lu real,
|
|
-- How many 400 ms gating blocks fell in each 0.1 LU bin. Bin i holds blocks
|
|
-- measuring -70.0 + (block_hist_start + i) / 10 LUFS; the array is trimmed
|
|
-- to the first and last non-empty bins. Album loudness is the gated loudness
|
|
-- of every block on the album, so it is computed from these exactly, with no
|
|
-- second decode (#4996).
|
|
block_hist_start smallint,
|
|
block_hist integer[],
|
|
unreadable boolean NOT NULL DEFAULT false,
|
|
analysis_version smallint NOT NULL,
|
|
analyzed_at timestamptz NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT track_loudness_hist_pair
|
|
CHECK ((block_hist IS NULL) = (block_hist_start IS NULL))
|
|
);
|
|
|
|
-- Loudness analysis's knobs, in admin Settings. Rule 25: an operator setting is
|
|
-- a database row, changed without a restart. Singleton in the style of
|
|
-- fingerprint_settings (0061).
|
|
CREATE TABLE loudness_settings (
|
|
id boolean PRIMARY KEY DEFAULT true,
|
|
-- Off stops the background analysis. Tracks already measured keep their
|
|
-- values, so normalization keeps working for them.
|
|
enabled boolean NOT NULL DEFAULT true,
|
|
-- Files analyzed at once. Each is a full decode, competing with playback
|
|
-- transcoding for CPU and with streaming for the mount.
|
|
backfill_concurrency integer NOT NULL DEFAULT 2,
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT loudness_settings_singleton CHECK (id = true),
|
|
CONSTRAINT loudness_settings_concurrency_range
|
|
CHECK (backfill_concurrency >= 1 AND backfill_concurrency <= 8)
|
|
);
|
|
INSERT INTO loudness_settings (id) VALUES (true) ON CONFLICT (id) DO NOTHING;
|