Skip to content

Commit

Permalink
playback: redocument/refactor gapless playback
Browse files Browse the repository at this point in the history
Should complete this feature, save regression fixes.

Resolves #110.
  • Loading branch information
OxygenCobalt committed Jan 15, 2024
1 parent 48ab83f commit b2d9b24
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 206 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,80 +25,218 @@ import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song

/**
* The designated "source of truth" for the current playback state. Should only be used by
* [PlaybackStateManager], which mirrors a more refined version of the state held here.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface PlaybackStateHolder {
/** The current [Progression] state of the audio player. */
val progression: Progression

/** The current [RepeatMode] of the audio player. */
val repeatMode: RepeatMode

/** The current [MusicParent] being played from. Null if playing from all songs. */
val parent: MusicParent?

/**
* Resolve the current queue state as a [RawQueue].
*
* @return The current queue state.
*/
fun resolveQueue(): RawQueue

/** The current audio session ID of the audio player. */
val audioSessionId: Int

/**
* Applies a completely new playback state to the holder.
*
* @param queue The new queue to use.
* @param start The song to start playback from. Should be in the queue.
* @param parent The parent to play from.
* @param shuffled Whether the queue should be shuffled.
*/
fun newPlayback(queue: List<Song>, start: Song?, parent: MusicParent?, shuffled: Boolean)

/**
* Update the playing state of the audio player.
*
* @param playing Whether the player should be playing audio.
*/
fun playing(playing: Boolean)

/**
* Seek to a position in the current song.
*
* @param positionMs The position to seek to, in milliseconds.
*/
fun seekTo(positionMs: Long)

/**
* Update the repeat mode of the audio player.
*
* @param repeatMode The new repeat mode.
*/
fun repeatMode(repeatMode: RepeatMode)

/** Go to the next song in the queue. */
fun next()

/** Go to the previous song in the queue. */
fun prev()

/**
* Go to a specific index in the queue.
*
* @param index The index to go to. Should be in the queue.
*/
fun goto(index: Int)

/**
* Add songs to the currently playing item in the queue.
*
* @param songs The songs to add.
* @param ack The [StateAck] to return to [PlaybackStateManager].
*/
fun playNext(songs: List<Song>, ack: StateAck.PlayNext)

/**
* Add songs to the end of the queue.
*
* @param songs The songs to add.
* @param ack The [StateAck] to return to [PlaybackStateManager].
*/
fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue)

/**
* Move a song in the queue to a new position.
*
* @param from The index of the song to move.
* @param to The index to move the song to.
* @param ack The [StateAck] to return to [PlaybackStateManager].
*/
fun move(from: Int, to: Int, ack: StateAck.Move)

/**
* Remove a song from the queue.
*
* @param at The index of the song to remove.
* @param ack The [StateAck] to return to [PlaybackStateManager].
* @return The [Song] that was removed.
*/
fun remove(at: Int, ack: StateAck.Remove)

/**
* Reorder the queue.
*
* @param shuffled Whether the queue should be shuffled.
*/
fun shuffled(shuffled: Boolean)

/**
* Handle a deferred playback action.
*
* @param action The action to handle.
* @return Whether the action could be handled, or if it should be deferred for later.
*/
fun handleDeferred(action: DeferredPlayback): Boolean

/**
* Override the current held state with a saved state.
*
* @param parent The parent to play from.
* @param rawQueue The queue to use.
* @param ack The [StateAck] to return to [PlaybackStateManager]. If null, do not return any
* ack.
*/
fun applySavedState(parent: MusicParent?, rawQueue: RawQueue, ack: StateAck.NewPlayback?)
}

/**
* An acknowledgement that the state of the [PlaybackStateHolder] has changed. This is sent back to
* [PlaybackStateManager] once an operation in [PlaybackStateHolder] has completed so that the new
* state can be mirrored to the rest of the application.
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed interface StateAck {
/**
* @see PlaybackStateHolder.next
* @see PlaybackStateHolder.prev
* @see PlaybackStateHolder.goto
*/
data object IndexMoved : StateAck

/** @see PlaybackStateHolder.playNext */
data class PlayNext(val at: Int, val size: Int) : StateAck

/** @see PlaybackStateHolder.addToQueue */
data class AddToQueue(val at: Int, val size: Int) : StateAck

/** @see PlaybackStateHolder.move */
data class Move(val from: Int, val to: Int) : StateAck

/** @see PlaybackStateHolder.remove */
data class Remove(val index: Int) : StateAck

/** @see PlaybackStateHolder.shuffled */
data object QueueReordered : StateAck

/**
* @see PlaybackStateHolder.newPlayback
* @see PlaybackStateHolder.applySavedState
*/
data object NewPlayback : StateAck

/**
* @see PlaybackStateHolder.playing
* @see PlaybackStateHolder.seekTo
*/
data object ProgressionChanged : StateAck

/** @see PlaybackStateHolder.repeatMode */
data object RepeatModeChanged : StateAck
}

/**
* The queue as it is represented in the audio player held by [PlaybackStateHolder]. This should not
* be used as anything but a container. Use the provided fields to obtain saner queue information.
*
* @param heap The ordered list of all [Song]s in the queue.
* @param shuffledMapping A list of indices that remap the songs in [heap] into a shuffled queue.
* Empty if the queue is not shuffled.
* @param heapIndex The index of the current song in [heap]. Note that if shuffled, this will be a
* nonsensical value that cannot be used to obtain next and last songs without first resolving the
* queue.
*/
data class RawQueue(
val heap: List<Song>,
val shuffledMapping: List<Int>,
val heapIndex: Int,
) {
/** Whether the queue is currently shuffled. */
val isShuffled = shuffledMapping.isNotEmpty()

/**
* Resolve and return the exact [Song] sequence in the queue.
*
* @return The [Song]s in the queue, in order.
*/
fun resolveSongs() =
if (isShuffled) {
shuffledMapping.map { heap[it] }
} else {
heap
}

/**
* Resolve and return the current index of the queue.
*
* @return The current index of the queue.
*/
fun resolveIndex() =
if (isShuffled) {
shuffledMapping.indexOf(heapIndex)
Expand All @@ -107,6 +245,7 @@ data class RawQueue(
}

companion object {
/** Create a blank instance. */
fun nil() = RawQueue(emptyList(), emptyList(), -1)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,25 @@ import org.oxycblt.auxio.util.logW
* @author Alexander Capehart (OxygenCobalt)
*/
interface PlaybackStateManager {
/** The current [Progression] state. */
/** The current [Progression] of the audio player */
val progression: Progression

/** The current [RepeatMode]. */
val repeatMode: RepeatMode

/** The current [MusicParent] being played from */
val parent: MusicParent?

/** The current [Song] being played. Null if nothing is playing. */
val currentSong: Song?

/** The current queue of [Song]s. */
val queue: List<Song>

/** The index of the currently playing [Song] in the queue. */
val index: Int

/** Whether the queue is shuffled or not. */
val isShuffled: Boolean

/** The audio session ID of the internal player. Null if no internal player exists. */
Expand Down
Loading

0 comments on commit b2d9b24

Please sign in to comment.