Playback engine
How the player is built — one ExoPlayer per track, crossfade, DJ filters, and the Cast handoff.
Playback is the least conventional part of the codebase. SimpMusic does not hand a playlist to the platform player and let it run — it manages the queue itself and drives one player instance per track.
Two engines, one interface
| Platform | Adapter | Backend |
|---|---|---|
| Android | CrossfadeExoPlayerAdapter | Media3 / ExoPlayer |
| Desktop | VlcPlayerAdapter | VLC via vlcj |
Both implement MediaPlayerInterface, so everything above them — ViewModels, the media
session, the UI — is platform-agnostic.
A stale comment in CrossfadeExoPlayerAdapter still refers to a GstreamerPlayerAdapter.
The desktop backend is VLC; the GStreamer path is gone.
Why one player per track
Crossfading requires two tracks producing audio at the same time, which a single player instance cannot do. So the adapter:
- keeps its own internal playlist rather than ExoPlayer's
- gives each track its own ExoPlayer, holding a single
MediaItem - precaches the upcoming track so the transition doesn't stall
- swaps listeners between instances as the active track changes
- exposes the whole thing to
MediaSessionthrough aDelegatingForwardingPlayer
Stream URLs are resolved lazily by MergingMediaSourceFactory plus a ResolvingDataSource,
so the adapter never has to extract URLs up front.
DJ-style transitions
CrossfadeFilterAudioProcessor is a Media3 AudioProcessor applying a real-time Biquad
filter during a crossfade:
- Low-pass on the outgoing track
- High-pass on the incoming track
- Cutoff frequency is animated across the transition
Two details worth knowing when working on it: the processor reports isActive = false when
disabled, so ExoPlayer skips it entirely and costs nothing during normal playback; and filter
coefficients are recomputed lazily, only when the cutoff or filter type actually changes. It
handles 16-bit PCM, mono and stereo.
Cast handoff
Because the adapter simulates the playlist itself, every ExoPlayer sits on a single-item
timeline — which means Media3's built-in state transfer cannot populate a Cast receiver's
queue. CastHandoffManager fills that gap by watching the session player's DeviceInfo:
| Transition | What the manager does |
|---|---|
| Local → remote | Snapshots index and position, silences local playback, pushes a resolved-URL window to the receiver |
| While remote | Routes transport calls to the session player, polls remote position |
| Remote → local | Restores the adapter at the last known remote position |
Resolve failures on the current track are counted, and auto-skip is capped, so a network outage cannot walk the whole queue.
