Improve volume normalization logic
This commit is contained in:
parent
61eeeb07c0
commit
f5716937d1
|
@ -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<Float?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(format: Format)
|
||||
|
||||
|
|
|
@ -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<String, Float?>()
|
||||
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() {
|
||||
|
|
Loading…
Reference in a new issue