feat: leveled FLAC stream for Sonos/UPnP speakers (M464 #5001)
release / go (push) Successful in 2m17s
release / govulncheck (push) Successful in 26s
release / web (push) Successful in 2m4s
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 / integration (push) Canceled after 4m39s
release / android (push) Canceled after 2m53s
release / Build signed APK (releases and dev) (push) Canceled after 2m24s
release / go (push) Successful in 2m17s
release / govulncheck (push) Successful in 26s
release / web (push) Successful in 2m4s
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 / integration (push) Canceled after 4m39s
release / android (push) Canceled after 2m53s
release / Build signed APK (releases and dev) (push) Canceled after 2m24s
Speakers fetch their own audio, so the phone cannot level it. A cast
token minted with level=true (and the client's asAlbum, which only the
queue holder knows) now returns GET /api/tracks/{id}/leveled.flac: the
track rendered by ffmpeg at the user's gain (volume=XdB, plus
alimiter at -1 dBFS for a limiter-mode boost), metadata stripped, FLAC
at 16 or 24 bits and at most 48 kHz. The gain is computed server-side
from the user's preference and the stored loudness, carried as
?g=<centi-dB>&lim=0|1 and signed into the token, so an edited URL does
not verify. Unity gains get the plain stream.
Renders are written beside the cache file and renamed in, keyed by the
source's size and mtime, coalesced per file (singleflight, detached
from the requesting speaker so a retry finds the render running),
started at mint time so the fetch finds them ready, and evicted least
recently used past leveled_cache_mb, a new admin setting (migration
0068, Loudness analysis card).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,7 +127,7 @@ func (q *Queries) GetLoudnessCoverage(ctx context.Context, currentVersion int16)
|
||||
}
|
||||
|
||||
const getLoudnessSettings = `-- name: GetLoudnessSettings :one
|
||||
SELECT id, enabled, backfill_concurrency, updated_at FROM loudness_settings WHERE id = true
|
||||
SELECT id, enabled, backfill_concurrency, updated_at, leveled_cache_mb FROM loudness_settings WHERE id = true
|
||||
`
|
||||
|
||||
func (q *Queries) GetLoudnessSettings(ctx context.Context) (LoudnessSetting, error) {
|
||||
@@ -138,6 +138,7 @@ func (q *Queries) GetLoudnessSettings(ctx context.Context) (LoudnessSetting, err
|
||||
&i.Enabled,
|
||||
&i.BackfillConcurrency,
|
||||
&i.UpdatedAt,
|
||||
&i.LeveledCacheMb,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -388,26 +389,29 @@ const updateLoudnessSettings = `-- name: UpdateLoudnessSettings :one
|
||||
UPDATE loudness_settings
|
||||
SET enabled = $1,
|
||||
backfill_concurrency = $2,
|
||||
leveled_cache_mb = $3,
|
||||
updated_at = now()
|
||||
WHERE id = true
|
||||
RETURNING id, enabled, backfill_concurrency, updated_at
|
||||
RETURNING id, enabled, backfill_concurrency, updated_at, leveled_cache_mb
|
||||
`
|
||||
|
||||
type UpdateLoudnessSettingsParams struct {
|
||||
Enabled bool
|
||||
BackfillConcurrency int32
|
||||
LeveledCacheMb int32
|
||||
}
|
||||
|
||||
// Whole-row write from the admin card; migration 0065's CHECK is the backstop
|
||||
// Whole-row write from the admin card; the migrations' CHECKs are the backstop
|
||||
// behind the service's own validation.
|
||||
func (q *Queries) UpdateLoudnessSettings(ctx context.Context, arg UpdateLoudnessSettingsParams) (LoudnessSetting, error) {
|
||||
row := q.db.QueryRow(ctx, updateLoudnessSettings, arg.Enabled, arg.BackfillConcurrency)
|
||||
row := q.db.QueryRow(ctx, updateLoudnessSettings, arg.Enabled, arg.BackfillConcurrency, arg.LeveledCacheMb)
|
||||
var i LoudnessSetting
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Enabled,
|
||||
&i.BackfillConcurrency,
|
||||
&i.UpdatedAt,
|
||||
&i.LeveledCacheMb,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@ type LoudnessSetting struct {
|
||||
Enabled bool
|
||||
BackfillConcurrency int32
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
LeveledCacheMb int32
|
||||
}
|
||||
|
||||
type MissingReacquisition struct {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE loudness_settings
|
||||
DROP CONSTRAINT loudness_settings_leveled_cache_range,
|
||||
DROP COLUMN leveled_cache_mb;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 0068_leveled_stream_cache.up.sql — the size cap of the leveled-stream render
|
||||
-- cache (Scribe milestone #464, #5001).
|
||||
--
|
||||
-- Sonos and other UPnP renderers fetch a track's URL themselves, so the phone
|
||||
-- cannot level what they play. The server renders a gain-applied FLAC for them
|
||||
-- instead and keeps it on disk, oldest-used first out once the cache is over
|
||||
-- this size. An operator setting (rule 25), on the loudness settings row it
|
||||
-- belongs with.
|
||||
ALTER TABLE loudness_settings
|
||||
ADD COLUMN leveled_cache_mb integer NOT NULL DEFAULT 2048,
|
||||
ADD CONSTRAINT loudness_settings_leveled_cache_range
|
||||
CHECK (leveled_cache_mb >= 256 AND leveled_cache_mb <= 65536);
|
||||
@@ -67,11 +67,12 @@ SELECT count(*)::bigint AS total,
|
||||
SELECT * FROM loudness_settings WHERE id = true;
|
||||
|
||||
-- name: UpdateLoudnessSettings :one
|
||||
-- Whole-row write from the admin card; migration 0065's CHECK is the backstop
|
||||
-- Whole-row write from the admin card; the migrations' CHECKs are the backstop
|
||||
-- behind the service's own validation.
|
||||
UPDATE loudness_settings
|
||||
SET enabled = sqlc.arg(enabled),
|
||||
backfill_concurrency = sqlc.arg(backfill_concurrency),
|
||||
leveled_cache_mb = sqlc.arg(leveled_cache_mb),
|
||||
updated_at = now()
|
||||
WHERE id = true
|
||||
RETURNING *;
|
||||
|
||||
Reference in New Issue
Block a user