Skip to content

Commit

Permalink
Implement normalization gain for music tracks
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsvanvelzen committed Jun 10, 2024
1 parent f0554b2 commit 4727056
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.jellyfin.playback.core.mediastream

import org.jellyfin.playback.core.element.ElementKey
import org.jellyfin.playback.core.element.element
import org.jellyfin.playback.core.queue.QueueEntry

private val normalizationGainKey = ElementKey<Float>("NormalizationGain")

/**
* Get or set the normalization gain for this [QueueEntry]. A supported backend will use this to
* apply a gain to the audio output. The normalization gain must target a loudness of -23LUFS.
*/
var QueueEntry.normalizationGain by element(normalizationGainKey)
37 changes: 37 additions & 0 deletions playback/exoplayer/src/main/kotlin/ExoPlayerAudioPipeline.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.jellyfin.playback.exoplayer

import android.media.audiofx.LoudnessEnhancer
import timber.log.Timber

class ExoPlayerAudioPipeline {
private var loudnessEnhancer: LoudnessEnhancer? = null
var normalizationGain: Float? = null
set(value) {
Timber.d("Normalization gain changed to $value")
field = value
applyGain()
}

fun setAudioSessionId(audioSessionId: Int) {
Timber.d("Audio session id changed to $audioSessionId")

// Re-creare loudness enhancer for normalization gain
loudnessEnhancer?.release()
loudnessEnhancer = LoudnessEnhancer(audioSessionId)

// Re-apply current normalization gain
applyGain()
}

private fun applyGain() {
val targetGain = normalizationGain
// Convert to millibels
?.times(100f)
// Round to integer
?.toInt()

Timber.d("Applying gain (targetGain=$targetGain)")
loudnessEnhancer?.setEnabled(targetGain != null)
loudnessEnhancer?.setTargetGain(targetGain ?: 0)
}
}
43 changes: 32 additions & 11 deletions playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.jellyfin.playback.core.backend.BasePlayerBackend
import org.jellyfin.playback.core.mediastream.MediaStream
import org.jellyfin.playback.core.mediastream.PlayableMediaStream
import org.jellyfin.playback.core.mediastream.mediaStream
import org.jellyfin.playback.core.mediastream.normalizationGain
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.core.model.PositionInfo
import org.jellyfin.playback.core.queue.QueueEntry
Expand All @@ -48,6 +49,7 @@ class ExoPlayerBackend(

private var currentStream: PlayableMediaStream? = null
private var subtitleView: SubtitleView? = null
private var audioPipeline = ExoPlayerAudioPipeline()

private val exoPlayer by lazy {
ExoPlayer.Builder(context)
Expand All @@ -58,24 +60,32 @@ class ExoPlayerBackend(
.setTrackSelector(DefaultTrackSelector(context).apply {
setParameters(buildUponParameters().apply {
setTunnelingEnabled(true)
setAudioOffloadPreferences(TrackSelectionParameters.AudioOffloadPreferences.DEFAULT.buildUpon().apply {
setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
}.build())
setAudioOffloadPreferences(
TrackSelectionParameters.AudioOffloadPreferences.DEFAULT.buildUpon().apply {
setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
}.build()
)
})
})
.setMediaSourceFactory(DefaultMediaSourceFactory(
context,
DefaultExtractorsFactory().apply {
val isLowRamDevice = context.getSystemService<ActivityManager>()?.isLowRamDevice == true
setTsExtractorTimestampSearchBytes(when (isLowRamDevice) {
true -> TS_SEARCH_BYTES_LM
false -> TS_SEARCH_BYTES_HM
})
val isLowRamDevice =
context.getSystemService<ActivityManager>()?.isLowRamDevice == true
setTsExtractorTimestampSearchBytes(
when (isLowRamDevice) {
true -> TS_SEARCH_BYTES_LM
false -> TS_SEARCH_BYTES_HM
}
)
}
))
.setPauseAtEndOfMediaItems(true)
.build()
.also { player -> player.addListener(PlayerListener()) }
.also { player ->
player.addListener(PlayerListener())
audioPipeline.setAudioSessionId(player.audioSessionId)
}
}

inner class PlayerListener : Player.Listener {
Expand Down Expand Up @@ -109,6 +119,15 @@ class ExoPlayerBackend(
listener?.onMediaStreamEnd(requireNotNull(currentStream))
}
}

override fun onAudioSessionIdChanged(audioSessionId: Int) {
audioPipeline.setAudioSessionId(audioSessionId)
}

override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
val queueEntry = mediaItem?.localConfiguration?.tag as? QueueEntry
audioPipeline.normalizationGain = queueEntry?.normalizationGain
}
}

override fun supportsStream(
Expand All @@ -132,7 +151,7 @@ class ExoPlayerBackend(
override fun prepareItem(item: QueueEntry) {
val stream = requireNotNull(item.mediaStream)
val mediaItem = MediaItem.Builder().apply {
setTag(stream)
setTag(item)
setMediaId(stream.hashCode().toString())
setUri(stream.url)
}.build()
Expand All @@ -153,7 +172,9 @@ class ExoPlayerBackend(

var streamIsPrepared = false
repeat(exoPlayer.mediaItemCount) { index ->
streamIsPrepared = streamIsPrepared || exoPlayer.getMediaItemAt(index).mediaId == stream.hashCode().toString()
streamIsPrepared =
streamIsPrepared || exoPlayer.getMediaItemAt(index).mediaId == stream.hashCode()
.toString()
}

if (!streamIsPrepared) prepareItem(item)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jellyfin.playback.jellyfin.queue

import org.jellyfin.playback.core.mediastream.normalizationGain
import org.jellyfin.playback.core.queue.QueueEntry
import org.jellyfin.playback.core.queue.QueueEntryMetadata
import org.jellyfin.playback.core.queue.metadata
Expand Down Expand Up @@ -44,6 +45,7 @@ fun createBaseItemQueueEntry(api: ApiClient, baseItem: BaseItemDto): QueueEntry
genre = baseItem.genres?.joinToString(", "),
)
entry.baseItem = baseItem
entry.normalizationGain = baseItem.normalizationGain
return entry
}

Expand Down

0 comments on commit 4727056

Please sign in to comment.