Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement normalization gain for music tracks #3660

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

Check warning

Code scanning / detekt

Report magic numbers. Magic number is a numeric literal that is not defined as a constant and hence it's unclear what the purpose of this number is. It's better to declare such numbers as constants and give them a proper name. By default, -1, 0, 1, and 2 are not considered to be magic numbers. Warning

This expression contains a magic number. Consider defining it to a well named constant.
// 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
Loading