playback: refactor queue persistence

Refactor the queue saved state system to make it easier to imlement in
the playback state database.
This commit is contained in:
Alexander Capehart 2023-01-08 09:52:56 -07:00
parent 82a9c08666
commit 0199d2f343
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 90 additions and 50 deletions

View file

@ -273,7 +273,5 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
dst.add(src.getOrNull(4)?.inRangeOrNull(0..59) ?: return) dst.add(src.getOrNull(4)?.inRangeOrNull(0..59) ?: return)
dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return) dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return)
} }
private fun transformYearToken(src: List<Int>, dst: MutableList<Int>) {}
} }
} }

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Core playback state controller class. * Core playback state controller class.
@ -518,7 +519,8 @@ class PlaybackStateManager private constructor() {
} }
// Sanitize the queue. // Sanitize the queue.
queue.remap { it.map(newLibrary::sanitize) } queue.applySavedState(
queue.toSavedState().remap { newLibrary.sanitize(unlikelyToBeNull(it)) })
notifyNewPlayback() notifyNewPlayback()

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.playback.state
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt import kotlin.random.nextInt
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
/** /**
@ -30,8 +31,8 @@ import org.oxycblt.auxio.music.Song
* interpreted into different queues depending on the current playback configuration. * interpreted into different queues depending on the current playback configuration.
* *
* In general, the implementation details don't need to be known for this data structure to be used, * In general, the implementation details don't need to be known for this data structure to be used,
* except in special circumstances like [remap]. The functions exposed should be familiar for any * except in special circumstances like [SavedState]. The functions exposed should be familiar for
* typical play queue. * any typical play queue.
* *
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@ -124,46 +125,6 @@ class Queue {
check() check()
} }
/**
* Replace the given heap with a new
* @param map Code to remap the existing [Song] heap into a new [Song] heap. This **MUST** be
* the same size as the original heap. [Song] instances that could not be converted should be
* replaced with null in the new heap.
* @throws IllegalStateException If the given invariants regarding [map] were violated.
*/
fun remap(map: (List<Song>) -> List<Song?>) {
val newHeap = map(heap)
val oldSong = currentSong
check(newHeap.size == heap.size) { "New heap must be the same size as original heap" }
val adjustments = mutableListOf<Int?>()
var currentShift = 0
for (song in newHeap) {
if (song != null) {
adjustments.add(currentShift)
} else {
adjustments.add(null)
currentShift -= 1
}
}
heap = newHeap.filterNotNull().toMutableList()
orderedMapping =
orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
adjustments[heapIndex]?.let { heapIndex + it }
}
shuffledMapping =
shuffledMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
adjustments[heapIndex]?.let { heapIndex + it }
}
// Make sure we re-align the index to point to the previously playing song.
while (currentSong != oldSong && index > -1) {
index--
}
check()
}
/** /**
* Add [Song]s to the top of the queue. Will start playback if nothing is playing. * Add [Song]s to the top of the queue. Will start playback if nothing is playing.
* @param songs The [Song]s to add. * @param songs The [Song]s to add.
@ -287,6 +248,52 @@ class Queue {
return result return result
} }
/**
* Convert the current state of this instance into a [SavedState].
* @return A new [SavedState] reflecting the exact state of the queue when called.
*/
fun toSavedState() =
SavedState(
heap.toList(),
orderedMapping.toList(),
shuffledMapping.toList(),
index,
currentSong?.uid)
/**
* Update this instance from the given [SavedState].
* @param savedState A [SavedState] with a valid queue representation.
*/
fun applySavedState(savedState: SavedState) {
val adjustments = mutableListOf<Int?>()
var currentShift = 0
for (song in savedState.heap) {
if (song != null) {
adjustments.add(currentShift)
} else {
adjustments.add(null)
currentShift -= 1
}
}
heap = savedState.heap.filterNotNull().toMutableList()
orderedMapping =
savedState.orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
adjustments[heapIndex]?.let { heapIndex + it }
}
shuffledMapping =
savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
adjustments[heapIndex]?.let { heapIndex + it }
}
// Make sure we re-align the index to point to the previously playing song.
index = savedState.currentIndex
while (currentSong?.uid != savedState.currentSongUid && index > -1) {
index--
}
check()
}
private fun addSongToHeap(song: Song): Int { private fun addSongToHeap(song: Song): Int {
// We want to first try to see if there are any "orphaned" songs in the queue // We want to first try to see if there are any "orphaned" songs in the queue
// that we can re-use. This way, we can reduce the memory used up by songs that // that we can re-use. This way, we can reduce the memory used up by songs that
@ -334,6 +341,36 @@ class Queue {
} }
} }
/**
* An immutable representation of the queue state.
* @param heap The heap of [Song]s that are/were used in the queue. This can be modified with
* null values to represent [Song]s that were "lost" from the heap without having to change
* other values.
* @param orderedMapping The mapping of the [heap] to an ordered queue.
* @param shuffledMapping The mapping of the [heap] to a shuffled queue.
* @param currentIndex The index of the currently playing [Song] at the time of serialization.
* @param currentSongUid The [Music.UID] of the [Song] that was originally at [currentIndex].
*/
class SavedState(
val heap: List<Song?>,
val orderedMapping: List<Int>,
val shuffledMapping: List<Int>,
val currentIndex: Int,
val currentSongUid: Music.UID?,
) {
/**
* Remaps the [heap] of this instance based on the given mapping function and copies it into
* a new [SavedState].
* @param transform Code to remap the existing [Song] heap into a new [Song] heap. This
* **MUST** be the same size as the original heap. [Song] instances that could not be
* converted should be replaced with null in the new heap.
* @throws IllegalStateException If the invariant specified by [transform] is violated.
*/
inline fun remap(transform: (Song?) -> Song?) =
SavedState(
heap.map(transform), orderedMapping, shuffledMapping, currentIndex, currentSongUid)
}
/** /**
* Represents the possible changes that can occur during certain queue mutation events. The * Represents the possible changes that can occur during certain queue mutation events. The
* precise meanings of these differ somewhat depending on the type of mutation done. * precise meanings of these differ somewhat depending on the type of mutation done.

View file

@ -17,11 +17,14 @@
package org.oxycblt.auxio.music.library package org.oxycblt.auxio.music.library
import org.oxycblt.auxio.music.Song
class LibraryTest { class LibraryTest {
fun library_common() {}
companion object { fun library_sparse() {}
val LIBRARY = listOf(Song.Raw())
} fun library_multiArtist() {}
fun library_multiGenre() {}
fun library_musicBrainz() {}
} }