| name | crossfade-exclusion-heuristics |
| description | Decide when a crossfade must NOT run — the item plays as video, the item is too short for the fade, or two consecutive items belong to the same album — and encode each rule so it survives shuffle, an auto-length fade and a queue that keeps growing. Use when a fade cuts a song short, a video jumps to its first frame under the previous track, a 25-second interlude spends half its length fading, or an album sequenced to run continuously is interrupted between every track. |
Crossfade exclusions
A crossfade is a default, not a law. Three exclusions earn their place; each is a small
predicate consulted at the trigger point, and each has a way of being written that quietly
does nothing.
val shouldCrossfade =
crossfadeEnabled && hasNextMediaItem() &&
!isCurrentTrackVideo() && !isNextTrackVideo() &&
!isCurrentTrackTooShortForCrossfade() && !isWithinAlbum()
Every one of these must also be present on the other path that starts a fade — see
guard-on-every-trigger-path; a predicate on one path only is dead code with no symptom.
Traps
The video check has to be symmetric, and it has to honour the user's setting. Two
separate rules, both needed:
private fun isNextTrackVideo() = watchVideoEnabled && next()?.isVideo() == true
private fun isCurrentTrackVideo() = watchVideoEnabled && currentMediaItem?.isVideo() == true
The next check exists because a video source is expensive and error-prone to prepare in
the middle of a fade, and because a video should start from its first frame rather than
fade in under the outgoing song. The check exists because a video should play out
to its last frame rather than fade out under the incoming one. The current-track half is
the one that gets deleted: an early version tested only "is this item a video?" and ignored
whether the user had video playback switched on at all, so it suppressed fades for
audio-only listeners. The fix is to make it symmetric with the next-track check, not to
remove it. is doing the real work in both.