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)
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
playbackModel.nextUp.observe(viewLifecycleOwner) {
// The queue icon uses a selector that will automatically tint the icon as active or
// 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 ->

View file

@ -22,7 +22,6 @@ import android.content.Context
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
@ -50,17 +49,16 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// Playback
private val mSong = MutableLiveData<Song?>()
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
private val mIsPlaying = MutableLiveData(false)
private val mIsShuffling = MutableLiveData(false)
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
private var mIntentUri: Uri? = null
@ -69,23 +67,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
val song: LiveData<Song?> get() = mSong
/** The current model that is being played from, such as an [Album] or [Artist] */
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 isShuffling: LiveData<Boolean> get() = mIsShuffling
/** The current repeat mode, see [LoopMode] for more information */
val loopMode: LiveData<LoopMode> get() = mLoopMode
/** The current playback position, in seconds */
val position: LiveData<Long> get() = mPosition
/** The queue, without the previous items. */
val nextItemsInQueue = Transformations.map(queue) { queue ->
queue.slice((mIndex.value!! + 1) until queue.size)
}
val nextUp: LiveData<List<Song>> get() = mNextUp
/** The current [PlaybackMode] that also determines the queue */
val playbackMode: LiveData<PlaybackMode> get() = mMode
private val playbackManager = PlaybackStateManager.maybeGetInstance()
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.
*/
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()
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.
*/
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 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()
playbackManager.moveQueueItems(from, to)
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.
*/
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
mPosition.value = playbackManager.position / 1000
mParent.value = playbackManager.parent
mQueue.value = playbackManager.queue
mNextUp.value = playbackManager.queue
mMode.value = playbackManager.playbackMode
mIndex.value = playbackManager.index
mIsPlaying.value = playbackManager.isPlaying
mIsShuffling.value = playbackManager.isShuffling
mLoopMode.value = playbackManager.loopMode
@ -369,12 +361,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
mPosition.value = position / 1000
}
override fun onQueueUpdate(queue: List<Song>) {
mQueue.value = queue
}
override fun onIndexUpdate(index: Int) {
mIndex.value = index
override fun onQueueUpdate(queue: List<Song>, index: Int) {
mNextUp.value = queue.slice(index.inc() until queue.size)
}
override fun onModeUpdate(mode: PlaybackMode) {

View file

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

View file

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

View file

@ -29,7 +29,6 @@ import android.content.pm.ServiceInfo
import android.media.AudioManager
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
@ -87,7 +86,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// System backend components
private lateinit var audioReactor: AudioReactor
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var widgets: WidgetController
private val systemReceiver = SystemEventReceiver()
@ -131,10 +129,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
// --- SYSTEM SETUP ---
audioReactor = AudioReactor(this, player)
wakeLock = getSystemServiceSafe(PowerManager::class).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, this::class.simpleName
)
widgets = WidgetController(this)
// Set up the media button callbacks
@ -195,7 +189,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
mediaSession.release()
audioReactor.release()
widgets.release()
releaseWakelock()
playbackManager.removeCallback(this)
settingsManager.removeCallback(this)
@ -216,10 +209,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
override fun onPlaybackStateChanged(state: Int) {
when (state) {
Player.STATE_READY -> {
startPollingPosition()
releaseWakelock()
}
Player.STATE_READY -> startPollingPosition()
Player.STATE_ENDED -> {
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) {
// If there's any issue, just go to the next song.
playbackManager.next()
@ -357,6 +342,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
return ExoPlayer.Builder(this, audioRenderer)
.setMediaSourceFactory(DefaultMediaSourceFactory(this, extractorsFactory))
.setWakeMode(C.WAKE_MODE_LOCAL)
.build()
}
@ -434,26 +420,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
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
* 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.
#### Additions that have already been rejected:
- ReplayGain [#7]
- Folder View/Grouping [#10]
- Recently added list [#18]
- Lyrics [#19]
- Tag editing [#33]
#### Additions that have already been rejected (and why):
- Folder View/Grouping [#10] (Out of scope)
- Recently added list [#18] (Out of scope)
- Lyrics [#19] (Out of scope)
- 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)