From f5716937d101671ddf7f3610a90e0736a19daefe Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Mon, 18 Jul 2022 14:36:57 +0200 Subject: [PATCH] Improve volume normalization logic --- .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 3 + .../vimusic/service/PlayerService.kt | 65 +++++++++++-------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 7b44f7e..aaab2d1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -114,6 +114,9 @@ interface Database { @Query("UPDATE SongPlaylistMap SET position = position + 1 WHERE playlistId = :playlistId AND position >= :fromPosition AND position <= :toPosition") fun incrementSongPositions(playlistId: Long, fromPosition: Int, toPosition: Int) + @Query("SELECT loudnessDb FROM Format WHERE songId = :songId") + fun loudnessDb(songId: String): Flow + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(format: Format) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt index 3856720..882ce4c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt @@ -51,6 +51,8 @@ import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.distinctUntilChanged import kotlin.math.roundToInt import kotlin.system.exitProcess import android.os.Binder as AndroidBinder @@ -85,7 +87,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job() - private val songPendingLoudnessDb = mutableMapOf() + private var volumeNormalizationJob: Job? = null private var isVolumeNormalizationEnabled = false private var isPersistentQueueEnabled = false @@ -124,7 +126,8 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene isVolumeNormalizationEnabled = preferences.getBoolean(volumeNormalizationKey, false) isInvincibilityEnabled = preferences.getBoolean(isInvincibilityEnabledKey, false) - val cacheEvictor = when (val size = preferences.getEnum(exoPlayerDiskCacheMaxSizeKey, ExoPlayerDiskCacheMaxSize.`2GB`)) { + val cacheEvictor = when (val size = + preferences.getEnum(exoPlayerDiskCacheMaxSizeKey, ExoPlayerDiskCacheMaxSize.`2GB`)) { ExoPlayerDiskCacheMaxSize.Unlimited -> NoOpCacheEvictor() else -> LeastRecentlyUsedCacheEvictor(size.bytes) } @@ -221,7 +224,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - normalizeVolume() + maybeNormalizeVolume() radio?.let { radio -> if (player.mediaItemCount - player.currentMediaItemIndex <= 3) { @@ -283,17 +286,27 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene } } - private fun normalizeVolume() { - player.volume = if (isVolumeNormalizationEnabled) { - player.currentMediaItem?.let { mediaItem -> - songPendingLoudnessDb.getOrElse(mediaItem.mediaId) { - mediaItem.mediaMetadata.extras?.getFloatOrNull("loudnessDb") - }?.takeIf { it > 0 }?.let { loudnessDb -> - (1f - (0.01f + loudnessDb / 14)).coerceIn(0.1f, 1f) - } - } ?: 1f - } else { - 1f + private fun maybeNormalizeVolume() { + if (!isVolumeNormalizationEnabled) { + volumeNormalizationJob?.cancel() + player.volume = 1f + return + } + + player.currentMediaItem?.mediaId?.let { songId -> + volumeNormalizationJob?.cancel() + volumeNormalizationJob = coroutineScope.launch(Dispatchers.IO) { + Database.loudnessDb(songId).cancellable().distinctUntilChanged() + .collect { loudnessDb -> + withContext(Dispatchers.Main) { + player.volume = if (loudnessDb != null && loudnessDb > 0) { + (1f - (0.01f + loudnessDb / 14)).coerceIn(0.1f, 1f) + } else { + 1f + } + } + } + } } } @@ -368,7 +381,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene volumeNormalizationKey -> { isVolumeNormalizationEnabled = sharedPreferences.getBoolean(key, isVolumeNormalizationEnabled) - normalizeVolume() + maybeNormalizeVolume() } isInvincibilityEnabledKey -> isInvincibilityEnabled = sharedPreferences.getBoolean(key, isInvincibilityEnabled) @@ -489,14 +502,6 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene val urlResult = runBlocking(Dispatchers.IO) { YouTube.player(videoId) }?.mapCatching { body -> - val loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat() - - songPendingLoudnessDb[videoId] = loudnessDb - - runBlocking(Dispatchers.Main) { - normalizeVolume() - } - when (val status = body.playabilityStatus.status) { "OK" -> body.streamingData?.adaptiveFormats?.findLast { format -> format.itag == 251 || format.itag == 140 @@ -514,7 +519,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene itag = format.itag, mimeType = format.mimeType, bitrate = format.bitrate, - loudnessDb = loudnessDb, + loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(), contentLength = format.contentLength, lastModified = format.lastModified ) @@ -575,10 +580,16 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene .build() return RenderersFactory { handler: Handler?, _, audioListener: AudioRendererEventListener?, _, _ -> - arrayOf( - MediaCodecAudioRenderer(this, MediaCodecSelector.DEFAULT, handler, audioListener, audioSink) + arrayOf( + MediaCodecAudioRenderer( + this, + MediaCodecSelector.DEFAULT, + handler, + audioListener, + audioSink ) - } + ) + } } inner class Binder : AndroidBinder() {