feat(android): level playback with a gain processor in the audio sink (M464 #5000)
release / go (push) Successful in 1m43s
release / web (push) Successful in 1m27s
release / govulncheck (push) Successful in 35s
release / integration (push) Successful in 5m16s
release / android (push) Failing after 3m52s
release / Build signed APK (releases and dev) (push) Failing after 3m20s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped

Media3 1.10.1 -> 1.11.0, the version Renovate proposes; 1.11 flushes the
sink's audio processors at every item boundary with the playlist timeline
and the new item's period. GainAudioProcessor uses that to find the track
and its play-order neighbours (auto mode's album rule) and applies the
gain from the first sample, gapless transitions included, with a -1 dBFS
peak limiter in limiter mode and a full-scale clamp otherwise.

Gains come from the library cache first (sync now carries track and album
ReplayGain values; Room v10 adds the columns and rewinds the sync cursor
so an existing cache re-pulls them), then GET /api/tracks/replay-gain,
then none. The player service refreshes the leveling preference at start.

Web: a same-album neighbour without a track number no longer counts as
in-order album play, matching Android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 19:10:35 -04:00
co-authored by Claude Opus 5.5
parent 38290bf8f9
commit 1013c283da
22 changed files with 902 additions and 17 deletions
+1
View File
@@ -76,6 +76,7 @@ describe('playingAsAlbum', () => {
test('neighbours without track numbers are not evidence of order', () => {
expect(playingAsAlbum([track('1', 'x'), track('2', 'x')], 0)).toBe(false);
expect(playingAsAlbum([track('1', 'x'), track('2', 'x', 3)], 1)).toBe(false);
});
});
+14 -12
View File
@@ -20,20 +20,22 @@ export const MAX_BOOST_DB = 12;
*/
export function playingAsAlbum(queue: TrackRef[], index: number): boolean {
const cur = queue[index];
if (!cur) return false;
const prev = queue[index - 1];
const next = queue[index + 1];
return (
(prev !== undefined && prev.album_id === cur.album_id && order(prev) < order(cur)) ||
(next !== undefined && next.album_id === cur.album_id && order(next) > order(cur))
);
const curOrder = cur ? order(cur) : null;
if (!cur || curOrder === null) return false;
const inOrder = (n: TrackRef | undefined, before: boolean): boolean => {
if (!n || n.album_id !== cur.album_id) return false;
const o = order(n);
if (o === null) return false;
return before ? o < curOrder : o > curOrder;
};
return inOrder(queue[index - 1], true) || inOrder(queue[index + 1], false);
}
// Disc-major track order. Tracks without numbers compare as equal and so
// never count as in order; a same-album neighbour with no numbers is not
// evidence of album play.
function order(t: TrackRef): number {
return (t.disc_number ?? 1) * 1000 + (t.track_number ?? 0);
// Disc-major track order. A track with no number has no place in the order
// and is never evidence of album play.
function order(t: TrackRef): number | null {
if (t.track_number == null) return null;
return (t.disc_number ?? 1) * 1000 + t.track_number;
}
/**