Files
minstrel/internal/db/migrations/0065_track_loudness.up.sql
T
bvandeusenandClaude Opus 5.5 6028d78844
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m33s
release / go (push) Successful in 1m49s
release / integration (push) Successful in 5m42s
release / android (push) Successful in 5m59s
release / Build signed APK (releases and dev) (push) Successful in 6m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
docs: migration headers keep the before and after, without ids, quotes or one library's numbers (#6277)
Applies rule 32's migration exception: each header still says what the
schema was, what changes and what it prevents, but task, milestone,
decision and rule ids are replaced by the practice they stood for, the
operator quotes by the decision, single-install counts are dropped, and
pointers to a deleted package and an external design doc are gone.
golang-migrate keeps no checksum, so editing applied migrations'
comments is safe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:34:39 -04:00

66 lines
3.5 KiB
SQL

-- 0065_track_loudness.up.sql — measured loudness per track, for loudness
-- normalization.
--
-- 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.
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;