playback: misc changes

I just spent 5 days trying to implement gapless playback using
ExoPlayer and Hopium. That's 5 days I'm never getting back. Heres
what I did add in the process though.
This commit is contained in:
OxygenCobalt 2021-12-27 18:34:43 -07:00
parent 25dd276bd8
commit 43e01e839d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 48 additions and 100 deletions

View file

@ -131,10 +131,10 @@ class PlaybackFragment : Fragment() {
binding.playbackSeekBar.setProgress(pos) binding.playbackSeekBar.setProgress(pos)
} }
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { playbackModel.nextUp.observe(viewLifecycleOwner) {
// The queue icon uses a selector that will automatically tint the icon as active or // The queue icon uses a selector that will automatically tint the icon as active or
// inactive. We just need to set the flag. // inactive. We just need to set the flag.
queueItem.isEnabled = playbackModel.nextItemsInQueue.value!!.isNotEmpty() queueItem.isEnabled = playbackModel.nextUp.value!!.isNotEmpty()
} }
playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying -> playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying ->

View file

@ -22,7 +22,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -50,17 +49,16 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// Playback // Playback
private val mSong = MutableLiveData<Song?>() private val mSong = MutableLiveData<Song?>()
private val mParent = MutableLiveData<MusicParent?>() private val mParent = MutableLiveData<MusicParent?>()
private val mPosition = MutableLiveData(0L)
// Queue
private val mQueue = MutableLiveData(listOf<Song>())
private val mIndex = MutableLiveData(0)
private val mMode = MutableLiveData(PlaybackMode.ALL_SONGS)
// States // States
private val mIsPlaying = MutableLiveData(false) private val mIsPlaying = MutableLiveData(false)
private val mIsShuffling = MutableLiveData(false) private val mIsShuffling = MutableLiveData(false)
private val mLoopMode = MutableLiveData(LoopMode.NONE) private val mLoopMode = MutableLiveData(LoopMode.NONE)
private val mPosition = MutableLiveData(0L)
// Queue
private val mNextUp = MutableLiveData(listOf<Song>())
private val mMode = MutableLiveData(PlaybackMode.ALL_SONGS)
// Other // Other
private var mIntentUri: Uri? = null private var mIntentUri: Uri? = null
@ -69,23 +67,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
val song: LiveData<Song?> get() = mSong val song: LiveData<Song?> get() = mSong
/** The current model that is being played from, such as an [Album] or [Artist] */ /** The current model that is being played from, such as an [Album] or [Artist] */
val parent: LiveData<MusicParent?> get() = mParent val parent: LiveData<MusicParent?> get() = mParent
/** The current playback position, in seconds */
val position: LiveData<Long> get() = mPosition
/** The current queue determined by [playbackMode] and [parent] */
val queue: LiveData<List<Song>> get() = mQueue
/** The current [PlaybackMode] that also determines the queue */
val playbackMode: LiveData<PlaybackMode> get() = mMode
val isPlaying: LiveData<Boolean> get() = mIsPlaying val isPlaying: LiveData<Boolean> get() = mIsPlaying
val isShuffling: LiveData<Boolean> get() = mIsShuffling val isShuffling: LiveData<Boolean> get() = mIsShuffling
/** The current repeat mode, see [LoopMode] for more information */ /** The current repeat mode, see [LoopMode] for more information */
val loopMode: LiveData<LoopMode> get() = mLoopMode val loopMode: LiveData<LoopMode> get() = mLoopMode
/** The current playback position, in seconds */
val position: LiveData<Long> get() = mPosition
/** The queue, without the previous items. */ /** The queue, without the previous items. */
val nextItemsInQueue = Transformations.map(queue) { queue -> val nextUp: LiveData<List<Song>> get() = mNextUp
queue.slice((mIndex.value!! + 1) until queue.size) /** The current [PlaybackMode] that also determines the queue */
} val playbackMode: LiveData<PlaybackMode> get() = mMode
private val playbackManager = PlaybackStateManager.maybeGetInstance() private val playbackManager = PlaybackStateManager.maybeGetInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
@ -220,9 +213,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* [apply] is called just before the change is committed so that the adapter can be updated. * [apply] is called just before the change is committed so that the adapter can be updated.
*/ */
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) { fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
val adjusted = adapterIndex + (mQueue.value!!.size - nextItemsInQueue.value!!.size) val adjusted = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size)
if (adjusted in mQueue.value!!.indices) { if (adjusted in mNextUp.value!!.indices) {
apply() apply()
playbackManager.removeQueueItem(adjusted) playbackManager.removeQueueItem(adjusted)
} }
@ -232,12 +225,12 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* [apply] is called just before the change is committed so that the adapter can be updated. * [apply] is called just before the change is committed so that the adapter can be updated.
*/ */
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean { fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean {
val delta = (mQueue.value!!.size - nextItemsInQueue.value!!.size) val delta = (playbackManager.queue.size - mNextUp.value!!.size)
val from = adapterFrom + delta val from = adapterFrom + delta
val to = adapterTo + delta val to = adapterTo + delta
if (from in mQueue.value!!.indices && to in mQueue.value!!.indices) { if (from in mNextUp.value!!.indices && to in mNextUp.value!!.indices) {
apply() apply()
playbackManager.moveQueueItems(from, to) playbackManager.moveQueueItems(from, to)
return true return true
@ -287,7 +280,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* Flip the shuffle status, e.g from on to off. Will keep song by default. * Flip the shuffle status, e.g from on to off. Will keep song by default.
*/ */
fun invertShuffleStatus() { fun invertShuffleStatus() {
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true) playbackManager.setShuffling(!playbackManager.isShuffling, true)
} }
/** /**
@ -343,9 +336,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
mSong.value = playbackManager.song mSong.value = playbackManager.song
mPosition.value = playbackManager.position / 1000 mPosition.value = playbackManager.position / 1000
mParent.value = playbackManager.parent mParent.value = playbackManager.parent
mQueue.value = playbackManager.queue mNextUp.value = playbackManager.queue
mMode.value = playbackManager.playbackMode mMode.value = playbackManager.playbackMode
mIndex.value = playbackManager.index
mIsPlaying.value = playbackManager.isPlaying mIsPlaying.value = playbackManager.isPlaying
mIsShuffling.value = playbackManager.isShuffling mIsShuffling.value = playbackManager.isShuffling
mLoopMode.value = playbackManager.loopMode mLoopMode.value = playbackManager.loopMode
@ -369,12 +361,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
mPosition.value = position / 1000 mPosition.value = position / 1000
} }
override fun onQueueUpdate(queue: List<Song>) { override fun onQueueUpdate(queue: List<Song>, index: Int) {
mQueue.value = queue mNextUp.value = queue.slice(index.inc() until queue.size)
}
override fun onIndexUpdate(index: Int) {
mIndex.value = index
} }
override fun onModeUpdate(mode: PlaybackMode) { override fun onModeUpdate(mode: PlaybackMode) {

View file

@ -67,7 +67,7 @@ class QueueFragment : Fragment() {
// --- VIEWMODEL SETUP ---- // --- VIEWMODEL SETUP ----
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { queue -> playbackModel.nextUp.observe(viewLifecycleOwner) { queue ->
if (queue.isEmpty()) { if (queue.isEmpty()) {
findNavController().navigateUp() findNavController().navigateUp()
return@observe return@observe

View file

@ -63,15 +63,7 @@ class PlaybackStateManager private constructor() {
// Queue // Queue
private var mQueue = mutableListOf<Song>() private var mQueue = mutableListOf<Song>()
set(value) {
field = value
callbacks.forEach { it.onQueueUpdate(value) }
}
private var mIndex = 0 private var mIndex = 0
set(value) {
field = value
callbacks.forEach { it.onIndexUpdate(value) }
}
private var mPlaybackMode = PlaybackMode.ALL_SONGS private var mPlaybackMode = PlaybackMode.ALL_SONGS
set(value) { set(value) {
field = value field = value
@ -107,8 +99,6 @@ class PlaybackStateManager private constructor() {
val position: Long get() = mPosition val position: Long get() = mPosition
/** The current queue determined by [parent] and [playbackMode] */ /** The current queue determined by [parent] and [playbackMode] */
val queue: List<Song> get() = mQueue val queue: List<Song> get() = mQueue
/** The current index of the queue */
val index: Int get() = mIndex
/** The current [PlaybackMode] */ /** The current [PlaybackMode] */
val playbackMode: PlaybackMode get() = mPlaybackMode val playbackMode: PlaybackMode get() = mPlaybackMode
/** Whether playback is paused or not */ /** Whether playback is paused or not */
@ -263,7 +253,7 @@ class PlaybackStateManager private constructor() {
updatePlayback(mQueue[mIndex], shouldPlay = mLoopMode == LoopMode.ALL) updatePlayback(mQueue[mIndex], shouldPlay = mLoopMode == LoopMode.ALL)
} }
forceQueueUpdate() pushQueueUpdate()
} }
/** /**
@ -280,7 +270,7 @@ class PlaybackStateManager private constructor() {
} }
updatePlayback(mQueue[mIndex]) updatePlayback(mQueue[mIndex])
forceQueueUpdate() pushQueueUpdate()
} }
} }
@ -300,7 +290,7 @@ class PlaybackStateManager private constructor() {
mQueue.removeAt(index) mQueue.removeAt(index)
forceQueueUpdate() pushQueueUpdate()
return true return true
} }
@ -318,7 +308,7 @@ class PlaybackStateManager private constructor() {
val item = mQueue.removeAt(from) val item = mQueue.removeAt(from)
mQueue.add(to, item) mQueue.add(to, item)
forceQueueUpdate() pushQueueUpdate()
return true return true
} }
@ -328,7 +318,7 @@ class PlaybackStateManager private constructor() {
*/ */
fun playNext(song: Song) { fun playNext(song: Song) {
mQueue.add(min(mIndex + 1, max(mQueue.lastIndex, 0)), song) mQueue.add(min(mIndex + 1, max(mQueue.lastIndex, 0)), song)
forceQueueUpdate() pushQueueUpdate()
} }
/** /**
@ -336,7 +326,7 @@ class PlaybackStateManager private constructor() {
*/ */
fun playNext(songs: List<Song>) { fun playNext(songs: List<Song>) {
mQueue.addAll(min(mIndex + 1, max(mQueue.lastIndex, 0)), songs) mQueue.addAll(min(mIndex + 1, max(mQueue.lastIndex, 0)), songs)
forceQueueUpdate() pushQueueUpdate()
} }
/** /**
@ -344,7 +334,7 @@ class PlaybackStateManager private constructor() {
*/ */
fun addToQueue(song: Song) { fun addToQueue(song: Song) {
mQueue.add(song) mQueue.add(song)
forceQueueUpdate() pushQueueUpdate()
} }
/** /**
@ -352,14 +342,16 @@ class PlaybackStateManager private constructor() {
*/ */
fun addToQueue(songs: List<Song>) { fun addToQueue(songs: List<Song>) {
mQueue.addAll(songs) mQueue.addAll(songs)
forceQueueUpdate() pushQueueUpdate()
} }
/** /**
* Force any callbacks to receive a queue update. * Force any callbacks to receive a queue update.
*/ */
private fun forceQueueUpdate() { private fun pushQueueUpdate() {
mQueue = mQueue callbacks.forEach {
it.onQueueUpdate(mQueue, mIndex)
}
} }
// --- SHUFFLE FUNCTIONS --- // --- SHUFFLE FUNCTIONS ---
@ -398,7 +390,7 @@ class PlaybackStateManager private constructor() {
mSong = mQueue[0] mSong = mQueue[0]
} }
forceQueueUpdate() pushQueueUpdate()
} }
/** /**
@ -424,7 +416,7 @@ class PlaybackStateManager private constructor() {
mIndex = mQueue.indexOf(lastSong) mIndex = mQueue.indexOf(lastSong)
} }
forceQueueUpdate() pushQueueUpdate()
} }
// --- STATE FUNCTIONS --- // --- STATE FUNCTIONS ---
@ -603,7 +595,7 @@ class PlaybackStateManager private constructor() {
} }
} }
forceQueueUpdate() pushQueueUpdate()
} }
/** /**
@ -632,9 +624,8 @@ class PlaybackStateManager private constructor() {
fun onSongUpdate(song: Song?) {} fun onSongUpdate(song: Song?) {}
fun onParentUpdate(parent: MusicParent?) {} fun onParentUpdate(parent: MusicParent?) {}
fun onPositionUpdate(position: Long) {} fun onPositionUpdate(position: Long) {}
fun onQueueUpdate(queue: List<Song>) {} fun onQueueUpdate(queue: List<Song>, index: Int) {}
fun onModeUpdate(mode: PlaybackMode) {} fun onModeUpdate(mode: PlaybackMode) {}
fun onIndexUpdate(index: Int) {}
fun onPlayingUpdate(isPlaying: Boolean) {} fun onPlayingUpdate(isPlaying: Boolean) {}
fun onShuffleUpdate(isShuffling: Boolean) {} fun onShuffleUpdate(isShuffling: Boolean) {}
fun onLoopUpdate(loopMode: LoopMode) {} fun onLoopUpdate(loopMode: LoopMode) {}

View file

@ -29,7 +29,6 @@ import android.content.pm.ServiceInfo
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
@ -87,7 +86,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// System backend components // System backend components
private lateinit var audioReactor: AudioReactor private lateinit var audioReactor: AudioReactor
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var widgets: WidgetController private lateinit var widgets: WidgetController
private val systemReceiver = SystemEventReceiver() private val systemReceiver = SystemEventReceiver()
@ -131,10 +129,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// --- SYSTEM SETUP --- // --- SYSTEM SETUP ---
audioReactor = AudioReactor(this, player) audioReactor = AudioReactor(this, player)
wakeLock = getSystemServiceSafe(PowerManager::class).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, this::class.simpleName
)
widgets = WidgetController(this) widgets = WidgetController(this)
// Set up the media button callbacks // Set up the media button callbacks
@ -195,7 +189,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
mediaSession.release() mediaSession.release()
audioReactor.release() audioReactor.release()
widgets.release() widgets.release()
releaseWakelock()
playbackManager.removeCallback(this) playbackManager.removeCallback(this)
settingsManager.removeCallback(this) settingsManager.removeCallback(this)
@ -216,10 +209,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
override fun onPlaybackStateChanged(state: Int) { override fun onPlaybackStateChanged(state: Int) {
when (state) { when (state) {
Player.STATE_READY -> { Player.STATE_READY -> startPollingPosition()
startPollingPosition()
releaseWakelock()
}
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
if (playbackManager.loopMode == LoopMode.TRACK) { if (playbackManager.loopMode == LoopMode.TRACK) {
@ -233,11 +223,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
} }
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
// We use the wakelock to ensure that the CPU is active while music is being loaded
acquireWakeLock()
}
override fun onPlayerError(error: PlaybackException) { override fun onPlayerError(error: PlaybackException) {
// If there's any issue, just go to the next song. // If there's any issue, just go to the next song.
playbackManager.next() playbackManager.next()
@ -357,6 +342,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
return ExoPlayer.Builder(this, audioRenderer) return ExoPlayer.Builder(this, audioRenderer)
.setMediaSourceFactory(DefaultMediaSourceFactory(this, extractorsFactory)) .setMediaSourceFactory(DefaultMediaSourceFactory(this, extractorsFactory))
.setWakeMode(C.WAKE_MODE_LOCAL)
.build() .build()
} }
@ -434,26 +420,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
isForeground = false isForeground = false
} }
/**
* Hold the wakelock for the default amount of time [25 Seconds]
*/
private fun acquireWakeLock() {
wakeLock.acquire(WAKELOCK_TIME)
logD("Wakelock is held.")
}
/**
* Release the wakelock if its currently being held.
*/
private fun releaseWakelock() {
if (wakeLock.isHeld) {
wakeLock.release()
logD("Wakelock is released.")
}
}
/** /**
* A [BroadcastReceiver] for receiving system events from notifications, widgets, or * A [BroadcastReceiver] for receiving system events from notifications, widgets, or
* headset plug events. * headset plug events.

View file

@ -21,9 +21,12 @@ This does not rule out these additions, but they are not accepted as often as ot
Feel free to fork Auxio to add your own feature set however. Feel free to fork Auxio to add your own feature set however.
#### Additions that have already been rejected: #### Additions that have already been rejected (and why):
- ReplayGain [#7]
- Folder View/Grouping [#10] - Folder View/Grouping [#10] (Out of scope)
- Recently added list [#18] - Recently added list [#18] (Out of scope)
- Lyrics [#19] - Lyrics [#19] (Out of scope)
- Tag editing [#33] - Tag editing [#33] (Out of scope)
- Gapless Playback [#35] (Technical issues)
- Reduce leading instrument [#45] (Technical issues, Out of scope)
- Opening music through a provider [#30] (Out of scope)