playback: rework audio focus

Rework audio focus to rely on the native ExoPlayer implementation
instead of a custom implementation.

Previously, we avoided ExoPlayer's AudioFocus system as it never
played after a transient lost. A few versions later now through,
now it does, so we may as well switch to it. This does introduce
a bug where ReplayGain functionality will conflict with audio
focus, but I hope to eliminate this with #115 as I switch to
an AudioProcessor instead of a callback.
This commit is contained in:
OxygenCobalt 2022-03-27 11:51:58 -06:00
parent e54a58c612
commit b748d73abb
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
37 changed files with 33 additions and 175 deletions

View file

@ -5,10 +5,15 @@
#### What's Fixed
- Fixed incorrect ellipsizing on song items
#### What's Changed
- Audio focus is no longer configurable
#### Dev/Meta
- Updated translations [Konstantin Tutsch -> German, cccClyde -> Chinese ]
- Switched to spotless and ktfmt instead of ktlint
- Migrated constants to centralized table
- Introduced new RecyclerView framework
- Use native ExoPlayer AudioFocus implementation
- Removed databinding [Greatly reduces compile times]
- A bunch of internal view implementation improvements

View file

@ -28,7 +28,6 @@ import org.oxycblt.auxio.coil.GenreImageFetcher
import org.oxycblt.auxio.coil.MusicKeyer
import org.oxycblt.auxio.settings.SettingsManager
/** TODO: Rework null-safety/usage of requireNotNull */
@Suppress("UNUSED")
class AuxioApp : Application(), ImageLoaderFactory {
override fun onCreate() {

View file

@ -100,7 +100,7 @@ class DetailViewModel : ViewModel() {
if (mCurrentAlbum.value?.id == id) return
val musicStore = MusicStore.requireInstance()
val album =
requireNotNull(musicStore.albums.find { it.id == id }) { "Invalid album ID provided " }
requireNotNull(musicStore.albums.find { it.id == id }) { "Invalid album id provided " }
mCurrentAlbum.value = album
refreshAlbumData(album)
@ -109,9 +109,7 @@ class DetailViewModel : ViewModel() {
fun setArtistId(id: Long) {
if (mCurrentArtist.value?.id == id) return
val musicStore = MusicStore.requireInstance()
val artist =
requireNotNull(musicStore.artists.find { it.id == id }) { "Invalid artist ID provided" }
val artist = requireNotNull(musicStore.artists.find { it.id == id }) {}
mCurrentArtist.value = artist
refreshArtistData(artist)
}
@ -119,8 +117,7 @@ class DetailViewModel : ViewModel() {
fun setGenreId(id: Long) {
if (mCurrentGenre.value?.id == id) return
val musicStore = MusicStore.requireInstance()
val genre =
requireNotNull(musicStore.genres.find { it.id == id }) { "Invalid genre ID provided" }
val genre = requireNotNull(musicStore.genres.find { it.id == id })
mCurrentGenre.value = genre
refreshGenreData(genre)
}

View file

@ -179,7 +179,8 @@ private constructor(
override fun bind(item: Album, listener: MenuItemListener) {
binding.parentImage.bindAlbumCover(item)
binding.parentName.textSafe = item.resolvedName
binding.parentInfo.textSafe = if (item.year != null) {
binding.parentInfo.textSafe =
if (item.year != null) {
binding.context.getString(R.string.fmt_number, item.year)
} else {
binding.context.getString(R.string.def_date)

View file

@ -49,7 +49,7 @@ sealed class Tab(open val mode: DisplayMode) {
companion object {
/** The length a well-formed tab sequence should be */
const val SEQUENCE_LEN = 4
private const val SEQUENCE_LEN = 4
/** The default tab sequence, represented in integer form */
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100

View file

@ -88,7 +88,7 @@ class PlaybackService :
private lateinit var notificationManager: NotificationManager
// System backend components
private lateinit var audioReactor: AudioReactor
private lateinit var audioReactor: VolumeReactor
private lateinit var widgets: WidgetController
private val systemReceiver = PlaybackReceiver()
@ -130,10 +130,10 @@ class PlaybackService :
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MUSIC)
.build(),
false)
true)
audioReactor =
AudioReactor(this) { volume ->
VolumeReactor { volume ->
logD("Updating player volume to $volume")
player.volume = volume
}
@ -191,7 +191,6 @@ class PlaybackService :
player.release()
connector.release()
mediaSession.release()
audioReactor.release()
widgets.release()
playbackManager.removeCallback(this)
@ -214,6 +213,13 @@ class PlaybackService :
// --- PLAYER EVENT LISTENER OVERRIDES ---
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (playbackManager.isPlaying != playWhenReady) {
playbackManager.setPlaying(playWhenReady)
}
}
override fun onPlaybackStateChanged(state: Int) {
when (state) {
Player.STATE_READY -> startPolling()
@ -281,7 +287,6 @@ class PlaybackService :
override fun onPlayingUpdate(isPlaying: Boolean) {
if (isPlaying && !player.isPlaying) {
player.play()
audioReactor.requestFocus()
startPolling()
} else {
player.pause()

View file

@ -17,13 +17,7 @@
package org.oxycblt.auxio.playback.system
import android.content.Context
import android.media.AudioManager
import android.os.Build
import androidx.core.math.MathUtils
import androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import com.google.android.exoplayer2.metadata.Metadata
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
@ -31,36 +25,24 @@ import kotlin.math.pow
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* Manages the current volume and playback state across ReplayGain and AudioFocus events.
* Manages the current volume across ReplayGain and AudioFocus events.
*
* TODO: Add ReplayGain pre-amp
*
* TODO: Add positive ReplayGain
* @author OxygenCobalt
*/
class AudioReactor(context: Context, private val callback: (Float) -> Unit) :
AudioManager.OnAudioFocusChangeListener, SettingsManager.Callback {
class VolumeReactor(private val callback: (Float) -> Unit) {
private data class Gain(val track: Float, val album: Float)
private data class GainTag(val key: String, val value: Float)
private val playbackManager = PlaybackStateManager.getInstance()
private val settingsManager = SettingsManager.getInstance()
private val audioManager = context.getSystemServiceSafe(AudioManager::class)
private val request =
AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
.setWillPauseWhenDucked(false)
.setAudioAttributes(
AudioAttributesCompat.Builder()
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
.build())
.setOnAudioFocusChangeListener(this)
.build()
private var pauseWasTransient = false
// It's good to keep the volume and the ducking multiplier separate so that we don't
// lose information
@ -77,23 +59,9 @@ class AudioReactor(context: Context, private val callback: (Float) -> Unit) :
callback(volume)
}
init {
settingsManager.addCallback(this)
}
/** Request the android system for audio focus */
fun requestFocus() {
logD("Requesting audio focus")
AudioManagerCompat.requestAudioFocus(audioManager, request)
}
/**
* Updates the rough volume adjustment for [Metadata] with ReplayGain tags. This is based off
* Vanilla Music's implementation.
*
* TODO: Add ReplayGain pre-amp
*
* TODO: Add positive ReplayGain
*/
fun applyReplayGain(metadata: Metadata?) {
if (metadata == null) {
@ -218,75 +186,9 @@ class AudioReactor(context: Context, private val callback: (Float) -> Unit) :
}
}
/** Abandon the current focus request and any callbacks */
fun release() {
AudioManagerCompat.abandonAudioFocusRequest(audioManager, request)
settingsManager.removeCallback(this)
}
// --- INTERNAL AUDIO FOCUS ---
override fun onAudioFocusChange(focusChange: Int) {
if (!settingsManager.doAudioFocus && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Don't do audio focus if its not enabled
return
}
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> onGain()
AudioManager.AUDIOFOCUS_LOSS -> onLossPermanent()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> onLossTransient()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> onDuck()
}
}
private fun onGain() {
if (multiplier == MULTIPLIER_DUCK) {
unduck()
} else if (pauseWasTransient) {
logD("Gained focus after transient loss")
// Play again if the pause was only temporary [AudioManager.AUDIOFOCUS_LOSS_TRANSIENT]
playbackManager.setPlaying(true)
pauseWasTransient = false
}
}
private fun onLossTransient() {
// Since this loss is only temporary, mark it as such if we had to pause playback.
if (playbackManager.isPlaying) {
logD("Pausing for transient loss")
playbackManager.setPlaying(false)
pauseWasTransient = true
}
}
private fun onLossPermanent() {
logD("Pausing for permanent loss")
playbackManager.setPlaying(false)
}
private fun onDuck() {
multiplier = MULTIPLIER_DUCK
logD("Ducked volume, now $volume")
}
private fun unduck() {
multiplier = 1f
logD("Unducked volume, now $volume")
}
// --- SETTINGS MANAGEMENT ---
override fun onAudioFocusUpdate(focus: Boolean) {
if (!focus) {
onGain()
}
}
companion object {
private const val MULTIPLIER_DUCK = 0.2f
const val RG_TRACK = "REPLAYGAIN_TRACK_GAIN"
const val RG_ALBUM = "REPLAYGAIN_ALBUM_GAIN"
const val R128_TRACK = "R128_TRACK_GAIN"

View file

@ -94,10 +94,6 @@ class SettingsManager private constructor(context: Context) :
val roundCovers: Boolean
get() = prefs.getBoolean(KEY_ROUND_COVERS, false)
/** Whether to do Audio focus. */
val doAudioFocus: Boolean
get() = prefs.getBoolean(KEY_AUDIO_FOCUS, true)
/** Whether to resume playback when a headset is connected (may not work well in all cases) */
val headsetAutoplay: Boolean
get() = prefs.getBoolean(KEY_HEADSET_AUTOPLAY, false)
@ -239,7 +235,6 @@ class SettingsManager private constructor(context: Context) :
KEY_SHOW_COVERS -> callbacks.forEach { it.onShowCoverUpdate(showCovers) }
KEY_QUALITY_COVERS -> callbacks.forEach { it.onQualityCoverUpdate(useQualityCovers) }
KEY_LIB_TABS -> callbacks.forEach { it.onLibTabsUpdate(libTabs) }
KEY_AUDIO_FOCUS -> callbacks.forEach { it.onAudioFocusUpdate(doAudioFocus) }
KEY_REPLAY_GAIN -> callbacks.forEach { it.onReplayGainUpdate(replayGainMode) }
}
}
@ -255,7 +250,6 @@ class SettingsManager private constructor(context: Context) :
fun onNotifActionUpdate(useAltAction: Boolean) {}
fun onShowCoverUpdate(showCovers: Boolean) {}
fun onQualityCoverUpdate(doQualityCovers: Boolean) {}
fun onAudioFocusUpdate(focus: Boolean) {}
fun onReplayGainUpdate(mode: ReplayGainMode) {}
}
@ -272,7 +266,6 @@ class SettingsManager private constructor(context: Context) :
const val KEY_ROUND_COVERS = "auxio_round_covers"
const val KEY_USE_ALT_NOTIFICATION_ACTION = "KEY_ALT_NOTIF_ACTION"
const val KEY_AUDIO_FOCUS = "KEY_AUDIO_FOCUS"
const val KEY_HEADSET_AUTOPLAY = "auxio_headset_autoplay"
const val KEY_REPLAY_GAIN = "auxio_replay_gain"

View file

@ -91,7 +91,7 @@ val Drawable.isRtl: Boolean
val ViewBinding.context: Context
get() = root.context
var TextView.textSafe
var TextView.textSafe: CharSequence
get() = text
set(value) {
text = value

View file

@ -68,7 +68,6 @@ class WidgetProvider : AppWidgetProvider() {
}
loadWidgetBitmap(context, song) { bitmap ->
logD(bitmap == null)
val state =
WidgetState(
song,

View file

@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:orientation="vertical"
tools:context=".settings.AboutFragment">

View file

@ -5,7 +5,6 @@
android:id="@+id/queue_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:orientation="vertical">
<org.oxycblt.auxio.ui.EdgeAppBarLayout

View file

@ -5,7 +5,6 @@
android:id="@+id/settings_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:orientation="vertical">
<org.oxycblt.auxio.ui.EdgeAppBarLayout

View file

@ -2,10 +2,8 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface">
android:layout_height="wrap_content">
<View
android:id="@+id/background"

View file

@ -81,8 +81,6 @@
<string name="set_alt_shuffle">تفضيل نشاط الخلط</string>
<string name="set_audio">صوتيات</string>
<string name="set_focus">تركيز الصوت</string>
<string name="set_focus_desc">ايقاف مؤقت عند تشغيل صوت آخر (كالمكالمات)</string>
<string name="set_replay_gain">صخب الصوت (تجريبي)</string>
<string name="set_off">اطفاء</string>
<string name="set_replay_gain_track">تفضيل المقطع</string>

View file

@ -65,8 +65,6 @@
<string name="set_alt_loop">"Preferovat akci režimu opakování"</string>
<string name="set_alt_shuffle">"Preferovat akci náhodného přehrávání"</string>
<string name="set_audio">"Zvuk"</string>
<string name="set_focus">"Zaměření zvuku"</string>
<string name="set_focus_desc">Pozastavit při přehrávání jiného zvuku (např. hovor)</string>
<string name="set_behavior">"Chování"</string>
<string name="set_song_mode">"Když je vybrána skladba"</string>
<string name="set_keep_shuffle">"Zapamatovat si náhodné přehrávání"</string>

View file

@ -71,8 +71,6 @@
<string name="set_alt_shuffle">Zufällig-Aktionstaste bevorzugen</string>
<string name="set_audio">Audio</string>
<string name="set_focus">Audiofokus</string>
<string name="set_focus_desc">Pausieren wenn andere Töne abspielt wird [Bsp. Anrufe]</string>
<string name="set_headset_autoplay">Kopfhörer automatische Wiedergabe</string>
<string name="set_headset_autoplay_desc">Beginne die Wiedergabe immer, wenn Kopfhörer verbunden sind (funktioniert nicht auf allen Geräten)</string>
<string name="set_replay_gain">ReplayGain (Experimentell)</string>

View file

@ -81,8 +81,6 @@
<string name="set_alt_shuffle">Preferir acción de mezcla</string>
<string name="set_audio">Sonido</string>
<string name="set_focus">Enfoque de sonido</string>
<string name="set_focus_desc">Pausar cuando se reproduce otro sonido (Ej: llamadas)</string>
<string name="set_replay_gain">ReplayGain (Experimental)</string>
<string name="set_off">Desactivado</string>
<string name="set_replay_gain_track">Por pista</string>

View file

@ -49,7 +49,6 @@
<string name="set_quality_covers">Ignorer le stockage des pochettes</string>
<string name="set_audio">Audio</string>
<string name="set_focus">Audio Focus</string>
<string name="set_behavior">Comportement</string>

View file

@ -40,7 +40,6 @@
<string name="set_accent">एक्सेंट</string>
<string name="set_audio">ऑडियो</string>
<string name="set_focus">ऑडियो फोकस</string>
<string name="set_behavior">चाल चलन</string>

View file

@ -48,7 +48,6 @@
<string name="set_quality_covers">A médiatár albumborítók figyelmen kívül hagyása</string>
<string name="set_audio">Hang</string>
<string name="set_focus">Hangfókusz</string>
<string name="set_behavior">Működés</string>

View file

@ -48,7 +48,6 @@
<string name="set_quality_covers">Abaikan sampul-sampul pada Media Penyimpanan</string>
<string name="set_audio">Audio</string>
<string name="set_focus">Fokus audio</string>
<string name="set_behavior">Perilaku</string>
<string name="set_keep_shuffle">Ingat putar acak</string>

View file

@ -49,7 +49,6 @@
<string name="set_quality_covers">Ignora le copertine del Media Store</string>
<string name="set_audio">Audio</string>
<string name="set_focus">Focus audio</string>
<string name="set_behavior">Comportamento</string>
<string name="set_keep_shuffle">Ricorda casuale</string>

View file

@ -47,7 +47,6 @@
<string name="set_quality_covers">미디어 스토어 앨범 커버 무시</string>
<string name="set_audio">오디오</string>
<string name="set_focus">오디오 포커스</string>
<string name="set_behavior">동작</string>

View file

@ -70,8 +70,6 @@
<string name="set_alt_shuffle">Voorkeur aan shuffle actie</string>
<string name="set_audio">Audio</string>
<string name="set_focus">Audiofocus</string>
<string name="set_focus_desc">Pauze wanneer andere audio speelt (ex. Gesprekken)</string>
<string name="set_behavior">Gedrag</string>
<string name="set_song_mode">Wanneer een liedje is geselecteerd</string>

View file

@ -48,7 +48,6 @@
<string name="set_quality_covers">Ignoruj okładki z Media Store</string>
<string name="set_audio">Dźwięk</string>
<string name="set_focus">Wyciszanie otoczenia</string>
<string name="set_behavior">Zachowanie</string>

View file

@ -48,7 +48,6 @@
<string name="set_quality_covers">Ignorar capas Media Store</string>
<string name="set_audio">Áudio</string>
<string name="set_focus">Foco do áudio</string>
<string name="set_behavior">Comportamento</string>
<string name="set_keep_shuffle">Memorizar aleatorização</string>

View file

@ -49,7 +49,6 @@
<string name="set_quality_covers">Ignorar capas Media Store</string>
<string name="set_audio">Áudio</string>
<string name="set_focus">Foco de áudio</string>
<string name="set_behavior">Comportamento</string>
<string name="set_keep_shuffle">Memorizar aleatorização</string>

View file

@ -49,7 +49,6 @@
<string name="set_quality_covers">Ignoră coperțile Media Store</string>
<string name="set_audio">Audio</string>
<string name="set_focus">Concentrare audio</string>
<string name="set_behavior">Comportament</string>

View file

@ -81,8 +81,6 @@
<string name="set_alt_shuffle">Режим перемешивания</string>
<string name="set_audio">Звук</string>
<string name="set_focus">Аудио-фокус</string>
<string name="set_focus_desc">Ставить на паузу при звонках</string>
<string name="set_replay_gain">ReplayGain (экспериментально)</string>
<string name="set_off">Выкл.</string>
<string name="set_replay_gain_track">По треку</string>

View file

@ -48,7 +48,6 @@
<string name="set_quality_covers">Medya Deposu albüm kapağını yoksay</string>
<string name="set_audio">Ses</string>
<string name="set_focus">Ses odaklama</string>
<string name="set_behavior">Tercihler</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="enable_theme_settings">false</bool>
<bool name="enable_audio_focus_setting">false</bool>
</resources>

View file

@ -80,8 +80,6 @@
<string name="set_alt_shuffle">偏好随机播放操作</string>
<string name="set_audio">音频</string>
<string name="set_focus">音频焦点</string>
<string name="set_focus_desc">有其它音频播放(比如电话)时暂停</string>
<string name="set_headset_autoplay">自动播放</string>
<string name="set_headset_autoplay_desc">连接至耳机时总是自动播放(并非在所有设备上都有用)</string>
<string name="set_replay_gain">回放增益(实验性)</string>

View file

@ -47,7 +47,6 @@
<string name="set_quality_covers">忽略音訊檔內嵌的專輯封面</string>
<string name="set_audio">音訊</string>
<string name="set_focus">音頻焦點</string>
<string name="set_behavior">行為</string>
<string name="set_keep_shuffle">記住隨機播放</string>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="enable_theme_settings">true</bool>
<bool name="enable_audio_focus_setting">true</bool>
<integer name="recycler_spans">1</integer>
</resources>

View file

@ -80,8 +80,6 @@
<string name="set_alt_shuffle">Prefer shuffle action</string>
<string name="set_audio">Audio</string>
<string name="set_focus">Audio focus</string>
<string name="set_focus_desc">Pause when other audio plays (ex. Calls)</string>
<string name="set_headset_autoplay">Headset autoplay</string>
<string name="set_headset_autoplay_desc">Always start playing when a headset is connected (may not work on all devices)</string>
<string name="set_replay_gain">ReplayGain (Experimental)</string>

View file

@ -78,14 +78,6 @@
app:layout="@layout/item_header"
app:title="@string/set_audio">
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
app:defaultValue="true"
app:iconSpaceReserved="false"
app:isPreferenceVisible="@bool/enable_audio_focus_setting"
app:key="KEY_AUDIO_FOCUS"
app:summary="@string/set_focus_desc"
app:title="@string/set_focus" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
app:defaultValue="false"
app:iconSpaceReserved="false"