Skip to content

Measure ReplayGain on device when the server sends no track gain - #241

Merged
ingoau merged 1 commit into
mainfrom
t3/replaygain-scan-untagged-tracks
Nov 11, 2026
Merged

ingoau merged 1 commit into
mainfrom
t3/replaygain-scan-untagged-tracks

Conversation

@ingoau

@ingoau ingoau commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

Part of #228 (desktop only for now: hocket-android builds hocket-core without the decode feature, so the scanner measures nothing on Android).

compute_replaygain (a complete EBU R128 scan) existed but nothing called it: downloads only copied the server's track.replay_gain, so tracks without ReplayGain tags — and transcodes that drop them — played with no level correction, which also made AutoMix jump in level mid-blend.

What this does

  • New GainScanner trait in the downloads module; the real implementation calls compute_replaygain (feature decode), injectable for tests.
  • Background worker: Downloads::new spawns one thread that takes (server, track) requests from a channel, deduplicated while pending, and exits when the Downloads is dropped. Nothing is measured on the playback or pin path.
  • Two scheduling points: a pin download completes without a server track gain (fetch_track), and a stream-cache entry is promoted to complete (cache_merge).
  • Startup catch-up: the scan queue lives in memory, so files that completed without being scanned (process died mid-scan) are found again on the next start; the worker's own skip checks make this cheap.
  • run_gain_scan skips tracks the server tags or that already have a stored gain, measures the pin's file (else the newest complete cache entry), and stores the result via set_gain (pin rows) or the new cache_entries.gain_db/peak columns (migration 0010). A decode failure stores nothing; the startup catch-up retries it on the next launch.
  • Downloads::gain_with_peak (and gain on top of it) reads a download's value, else the newest measured cache entry (original preferred over a transcode), so resolve/media_source_for level untagged tracks that were never pinned. In media_source_for, the measured track gain fills what the server left out — a partial tag set (album gain, a bare peak) no longer shadows the on-device scan, and the measured peak reaches the clip clamp.
  • Adoption keeps its gain: a cache file adopted as a pin's download carries its measured gain onto the pin rows instead of being re-measured.
  • The module doc and the compute_replaygain doc now match reality.

Tests

Five new tests around an injected fake scanner: untagged download measured once (with peak, and resolve picks the gain up); a server-reported gain never triggers a scan; a complete cache entry is measured for a non-pinned track (stored on the cache row, resolve picks it up); an undecodable file stays gainless without rescans; and a fresh process scans files completed while it was away.

cargo test -p hocket-core: 877 passed. Verified with and without the decode feature, fmt and clippy clean (only pre-existing warnings remain).

Generated by GLM-5.3-Flash in OpenCode

compute_replaygain existed but nothing called it: downloads only copied
the server's track.replay_gain, so untagged tracks (and transcodes that
drop the tags) played with no level correction, jumping mid-blend in
AutoMix. Now a background worker thread measures the finished file once
— the pin's download, or a complete stream-cache entry — and stores the
result on the pin rows or the cache entry; Downloads::gain falls back to
the cache entry so levelling works for untagged tracks that were never
pinned. The scan runs off the playback path on its own thread, gated on
the decode feature, injectable for tests.

Co-authored-by: GLM-5.3-Flash <opencode-agent[bot]@users.noreply.github.com>
@ingoau
ingoau force-pushed the t3/replaygain-scan-untagged-tracks branch from 9ebf5f2 to 24c1343 Compare November 10, 2026 12:07
@ingoau
ingoau merged commit 8ac16eb into main Nov 11, 2026
13 checks passed
@ingoau
ingoau deleted the t3/replaygain-scan-untagged-tracks branch November 11, 2026 00:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant