playback: reformat

This commit is contained in:
Alexander Capehart 2024-09-24 18:41:13 -06:00
parent f245e33887
commit 2c87aa5830
5 changed files with 146 additions and 34 deletions

View file

@ -1,3 +1,21 @@
/*
* Copyright (c) 2024 Auxio Project
* GaplessQueuer.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.player package org.oxycblt.auxio.playback.player
import androidx.media3.common.C import androidx.media3.common.C
@ -5,15 +23,20 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Player.RepeatMode import androidx.media3.common.Player.RepeatMode
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import org.oxycblt.auxio.playback.PlaybackSettings
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.playback.PlaybackSettings
/** /** */
* class GaplessQueuer
*/ private constructor(
class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer, private val listener: Queuer.Listener, private val playbackSettings: PlaybackSettings) : Queuer, PlaybackSettings.Listener, Player.Listener { private val exoPlayer: ExoPlayer,
class Factory @Inject constructor(private val playbackSettings: PlaybackSettings) : Queuer.Factory { private val listener: Queuer.Listener,
override fun create(exoPlayer: ExoPlayer, listener: Queuer.Listener) = GaplessQueuer(exoPlayer, listener, playbackSettings) private val playbackSettings: PlaybackSettings
) : Queuer, PlaybackSettings.Listener, Player.Listener {
class Factory @Inject constructor(private val playbackSettings: PlaybackSettings) :
Queuer.Factory {
override fun create(exoPlayer: ExoPlayer, listener: Queuer.Listener) =
GaplessQueuer(exoPlayer, listener, playbackSettings)
} }
override val currentMediaItem: MediaItem? = exoPlayer.currentMediaItem override val currentMediaItem: MediaItem? = exoPlayer.currentMediaItem
@ -86,9 +109,13 @@ class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer, privat
override fun goto(mediaItemIndex: Int) = exoPlayer.seekTo(mediaItemIndex, C.TIME_UNSET) override fun goto(mediaItemIndex: Int) = exoPlayer.seekTo(mediaItemIndex, C.TIME_UNSET)
override fun seekToNext() = exoPlayer.seekToNext() override fun seekToNext() = exoPlayer.seekToNext()
override fun hasNextMediaItem() = exoPlayer.hasNextMediaItem() override fun hasNextMediaItem() = exoPlayer.hasNextMediaItem()
override fun seekToPrevious() = exoPlayer.seekToPrevious() override fun seekToPrevious() = exoPlayer.seekToPrevious()
override fun seekToPreviousMediaItem() = exoPlayer.seekToPreviousMediaItem() override fun seekToPreviousMediaItem() = exoPlayer.seekToPreviousMediaItem()
override fun hasPreviousMediaItem() = exoPlayer.hasPreviousMediaItem() override fun hasPreviousMediaItem() = exoPlayer.hasPreviousMediaItem()
override fun prepareNew(mediaItems: List<MediaItem>, startIndex: Int?, shuffled: Boolean) { override fun prepareNew(mediaItems: List<MediaItem>, startIndex: Int?, shuffled: Boolean) {
@ -102,7 +129,12 @@ class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer, privat
exoPlayer.prepare() exoPlayer.prepare()
} }
override fun prepareSaved(mediaItems: List<MediaItem>, mapping: List<Int>, index: Int, shuffled: Boolean) { override fun prepareSaved(
mediaItems: List<MediaItem>,
mapping: List<Int>,
index: Int,
shuffled: Boolean
) {
exoPlayer.setMediaItems(mediaItems) exoPlayer.setMediaItems(mediaItems)
if (shuffled) { if (shuffled) {
exoPlayer.shuffleModeEnabled = true exoPlayer.shuffleModeEnabled = true
@ -125,7 +157,9 @@ class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer, privat
C.INDEX_UNSET C.INDEX_UNSET
} else { } else {
currTimeline.getNextWindowIndex( currTimeline.getNextWindowIndex(
exoPlayer.currentMediaItemIndex, Player.REPEAT_MODE_OFF, exoPlayer.shuffleModeEnabled) exoPlayer.currentMediaItemIndex,
Player.REPEAT_MODE_OFF,
exoPlayer.shuffleModeEnabled)
} }
if (nextIndex == C.INDEX_UNSET) { if (nextIndex == C.INDEX_UNSET) {
@ -161,8 +195,7 @@ class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer, privat
if (exoPlayer.shuffleModeEnabled) { if (exoPlayer.shuffleModeEnabled) {
// Have to manually refresh the shuffle seed and anchor it to the new current songs // Have to manually refresh the shuffle seed and anchor it to the new current songs
exoPlayer.setShuffleOrder( exoPlayer.setShuffleOrder(
BetterShuffleOrder(exoPlayer.mediaItemCount, exoPlayer.currentMediaItemIndex) BetterShuffleOrder(exoPlayer.mediaItemCount, exoPlayer.currentMediaItemIndex))
)
} }
} }
@ -184,4 +217,4 @@ class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer, privat
exoPlayer.pause() exoPlayer.pause()
} }
} }
} }

View file

@ -1,3 +1,21 @@
/*
* Copyright (c) 2024 Auxio Project
* PlayerKernel.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.player package org.oxycblt.auxio.playback.player
import android.content.Context import android.content.Context
@ -12,8 +30,8 @@ import androidx.media3.exoplayer.audio.AudioCapabilities
import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
interface PlayerKernel { interface PlayerKernel {
val isPlaying: Boolean val isPlaying: Boolean
@ -23,28 +41,47 @@ interface PlayerKernel {
val queuer: Queuer val queuer: Queuer
fun attach() fun attach()
fun release() fun release()
fun play() fun play()
fun pause() fun pause()
fun seekTo(positionMs: Long) fun seekTo(positionMs: Long)
fun replaceQueuer(queuerFactory: Queuer.Factory) fun replaceQueuer(queuerFactory: Queuer.Factory)
interface Listener { interface Listener {
fun onPlayWhenReadyChanged() fun onPlayWhenReadyChanged()
fun onIsPlayingChanged() fun onIsPlayingChanged()
fun onPositionDiscontinuity() fun onPositionDiscontinuity()
fun onError(error: PlaybackException) fun onError(error: PlaybackException)
} }
interface Factory { interface Factory {
fun create(context: Context, playerListener: Listener, queuerFactory: Queuer.Factory, queuerListener: Queuer.Listener): PlayerKernel fun create(
context: Context,
playerListener: Listener,
queuerFactory: Queuer.Factory,
queuerListener: Queuer.Listener
): PlayerKernel
} }
} }
class PlayerKernelFactoryImpl(@Inject private val mediaSourceFactory: MediaSource.Factory, @Inject private val replayGainProcessor: ReplayGainAudioProcessor) : PlayerKernel.Factory { class PlayerKernelFactoryImpl(
override fun create(context: Context, playerListener: PlayerKernel.Listener, queuerFactory: Queuer.Factory, queuerListener: Queuer.Listener): PlayerKernel { @Inject private val mediaSourceFactory: MediaSource.Factory,
@Inject private val replayGainProcessor: ReplayGainAudioProcessor
) : PlayerKernel.Factory {
override fun create(
context: Context,
playerListener: PlayerKernel.Listener,
queuerFactory: Queuer.Factory,
queuerListener: Queuer.Listener
): PlayerKernel {
// Since Auxio is a music player, only specify an audio renderer to save // Since Auxio is a music player, only specify an audio renderer to save
// battery/apk size/cache size // battery/apk size/cache size
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ -> val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
@ -71,9 +108,10 @@ class PlayerKernelFactoryImpl(@Inject private val mediaSourceFactory: MediaSourc
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(), .build(),
true) true)
.build() .build()
return PlayerKernelImpl(exoPlayer, replayGainProcessor, playerListener, queuerListener, queuerFactory) return PlayerKernelImpl(
exoPlayer, replayGainProcessor, playerListener, queuerListener, queuerFactory)
} }
} }
@ -85,14 +123,20 @@ private class PlayerKernelImpl(
queuerFactory: Queuer.Factory queuerFactory: Queuer.Factory
) : PlayerKernel, Player.Listener { ) : PlayerKernel, Player.Listener {
override var queuer: Queuer = queuerFactory.create(exoPlayer, queuerListener) override var queuer: Queuer = queuerFactory.create(exoPlayer, queuerListener)
override val isPlaying: Boolean get() = exoPlayer.isPlaying override val isPlaying: Boolean
get() = exoPlayer.isPlaying
override var playWhenReady: Boolean override var playWhenReady: Boolean
get() = exoPlayer.playWhenReady get() = exoPlayer.playWhenReady
set(value) { set(value) {
exoPlayer.playWhenReady = value exoPlayer.playWhenReady = value
} }
override val currentPosition: Long get() = exoPlayer.currentPosition
override val audioSessionId: Int get() = exoPlayer.audioSessionId override val currentPosition: Long
get() = exoPlayer.currentPosition
override val audioSessionId: Int
get() = exoPlayer.audioSessionId
override fun attach() { override fun attach() {
exoPlayer.addListener(this) exoPlayer.addListener(this)
@ -141,4 +185,4 @@ private class PlayerKernelImpl(
super.onPlayerError(error) super.onPlayerError(error)
playerListener.onError(error) playerListener.onError(error)
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * Copyright (c) 2023 Auxio Project
* SystemModule.kt is part of Auxio. * PlayerModule.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* ExoPlaybackStateHolder.kt is part of Auxio. * PlayerStateHolder.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,7 +24,6 @@ import android.media.audiofx.AudioEffect
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -55,12 +54,12 @@ import org.oxycblt.auxio.util.logE
class PlayerStateHolder( class PlayerStateHolder(
private val context: Context, private val context: Context,
playerKernelFactory: PlayerKernel.Factory, playerKernelFactory: PlayerKernel.Factory,
gaplessQueuerFactory: Queuer.Factory,
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
private val persistenceRepository: PersistenceRepository, private val persistenceRepository: PersistenceRepository,
private val playbackSettings: PlaybackSettings, private val playbackSettings: PlaybackSettings,
private val commandFactory: PlaybackCommand.Factory, private val commandFactory: PlaybackCommand.Factory,
private val replayGainProcessor: ReplayGainAudioProcessor, private val replayGainProcessor: ReplayGainAudioProcessor,
gaplessQueuerFactory: Queuer.Factory,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val imageSettings: ImageSettings private val imageSettings: ImageSettings
) : ) :
@ -76,9 +75,9 @@ class PlayerStateHolder(
private val persistenceRepository: PersistenceRepository, private val persistenceRepository: PersistenceRepository,
private val playbackSettings: PlaybackSettings, private val playbackSettings: PlaybackSettings,
private val playerFactory: PlayerKernel.Factory, private val playerFactory: PlayerKernel.Factory,
private val gaplessQueuerFactory: Queuer.Factory,
private val commandFactory: PlaybackCommand.Factory, private val commandFactory: PlaybackCommand.Factory,
private val replayGainProcessor: ReplayGainAudioProcessor, private val replayGainProcessor: ReplayGainAudioProcessor,
private val gaplessQueuerFactory: Queuer.Factory,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val imageSettings: ImageSettings, private val imageSettings: ImageSettings,
) { ) {
@ -86,12 +85,12 @@ class PlayerStateHolder(
return PlayerStateHolder( return PlayerStateHolder(
context, context,
playerFactory, playerFactory,
gaplessQueuerFactory,
playbackManager, playbackManager,
persistenceRepository, persistenceRepository,
playbackSettings, playbackSettings,
commandFactory, commandFactory,
replayGainProcessor, replayGainProcessor,
gaplessQueuerFactory,
musicRepository, musicRepository,
imageSettings) imageSettings)
} }
@ -149,8 +148,10 @@ class PlayerStateHolder(
override fun resolveQueue(): RawQueue { override fun resolveQueue(): RawQueue {
val heap = player.queuer.computeHeap() val heap = player.queuer.computeHeap()
val shuffledMapping = if (player.queuer.shuffleModeEnabled) player.queuer.computeMapping() else emptyList() val shuffledMapping =
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, player.queuer.currentMediaItemIndex) if (player.queuer.shuffleModeEnabled) player.queuer.computeMapping() else emptyList()
return RawQueue(
heap.mapNotNull { it.song }, shuffledMapping, player.queuer.currentMediaItemIndex)
} }
override fun handleDeferred(action: DeferredPlayback): Boolean { override fun handleDeferred(action: DeferredPlayback): Boolean {
@ -246,7 +247,8 @@ class PlayerStateHolder(
// Replicate the old pseudo-circular queue behavior when no repeat option is implemented. // Replicate the old pseudo-circular queue behavior when no repeat option is implemented.
// Basically, you can't skip back and wrap around the queue, but you can skip forward and // Basically, you can't skip back and wrap around the queue, but you can skip forward and
// wrap around the queue, albeit playback will be paused. // wrap around the queue, albeit playback will be paused.
if (player.queuer.repeatMode == Player.REPEAT_MODE_ALL || player.queuer.hasNextMediaItem()) { if (player.queuer.repeatMode == Player.REPEAT_MODE_ALL ||
player.queuer.hasNextMediaItem()) {
player.queuer.seekToNext() player.queuer.seekToNext()
if (!playbackSettings.rememberPause) { if (!playbackSettings.rememberPause) {
player.play() player.play()
@ -346,7 +348,11 @@ class PlayerStateHolder(
sendEvent = true sendEvent = true
} }
if (rawQueue != resolveQueue()) { if (rawQueue != resolveQueue()) {
player.queuer.prepareSaved(rawQueue.heap.map { it.buildMediaItem() }, rawQueue.shuffledMapping, rawQueue.heapIndex, rawQueue.isShuffled) player.queuer.prepareSaved(
rawQueue.heap.map { it.buildMediaItem() },
rawQueue.shuffledMapping,
rawQueue.heapIndex,
rawQueue.isShuffled)
player.pause() player.pause()
sendEvent = true sendEvent = true
} }

View file

@ -1,7 +1,24 @@
/*
* Copyright (c) 2024 Auxio Project
* Queuer.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.player package org.oxycblt.auxio.playback.player
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Player.RepeatMode import androidx.media3.common.Player.RepeatMode
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
@ -13,30 +30,42 @@ interface Queuer {
@get:RepeatMode var repeatMode: Int @get:RepeatMode var repeatMode: Int
fun attach() fun attach()
fun release() fun release()
fun goto(mediaItemIndex: Int) fun goto(mediaItemIndex: Int)
fun seekToNext() fun seekToNext()
fun hasNextMediaItem(): Boolean fun hasNextMediaItem(): Boolean
fun seekToPrevious() fun seekToPrevious()
fun seekToPreviousMediaItem() fun seekToPreviousMediaItem()
fun hasPreviousMediaItem(): Boolean fun hasPreviousMediaItem(): Boolean
fun moveMediaItem(fromIndex: Int, toIndex: Int) fun moveMediaItem(fromIndex: Int, toIndex: Int)
fun removeMediaItem(index: Int) fun removeMediaItem(index: Int)
// EXTENSIONS // EXTENSIONS
fun computeHeap(): List<MediaItem> fun computeHeap(): List<MediaItem>
fun computeMapping(): List<Int> fun computeMapping(): List<Int>
fun computeFirstMediaItemIndex(): Int fun computeFirstMediaItemIndex(): Int
fun prepareNew(mediaItems: List<MediaItem>, startIndex: Int?, shuffled: Boolean) fun prepareNew(mediaItems: List<MediaItem>, startIndex: Int?, shuffled: Boolean)
fun prepareSaved(mediaItems: List<MediaItem>, mapping: List<Int>, index: Int, shuffled: Boolean) fun prepareSaved(mediaItems: List<MediaItem>, mapping: List<Int>, index: Int, shuffled: Boolean)
fun discard() fun discard()
fun addTopMediaItems(mediaItems: List<MediaItem>) fun addTopMediaItems(mediaItems: List<MediaItem>)
fun addBottomMediaItems(mediaItems: List<MediaItem>) fun addBottomMediaItems(mediaItems: List<MediaItem>)
fun shuffled(shuffled: Boolean) fun shuffled(shuffled: Boolean)
interface Listener { interface Listener {
@ -46,4 +75,4 @@ interface Queuer {
interface Factory { interface Factory {
fun create(exoPlayer: ExoPlayer, listener: Listener): Queuer fun create(exoPlayer: ExoPlayer, listener: Listener): Queuer
} }
} }