Skip to content
Open
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
Expand Up @@ -96,7 +96,6 @@ class PlaybackManager(
appCoroutineScope.launch {
playback.load(current, next, seekPosition) { result ->
result.onSuccess {
playback.setReplayGain(trackGain = current.replayGainTrack, albumGain = current.replayGainAlbum)
completion(Result.success(attempt == 1))
}
result.onFailure { error ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,36 @@ import com.simplecityapps.playback.exoplayer.ByteUtils.getInt24
import com.simplecityapps.playback.exoplayer.ByteUtils.putInt24
import java.nio.ByteBuffer

/**
* Audio processor that applies ReplayGain volume normalization.
*
* Thread Safety:
* - trackGain and albumGain are updated from the main thread (via setReplayGain)
* - gain getter is accessed from the audio rendering thread (in queueInput)
* - @Synchronized ensures thread-safe access
*
* Note on Timing:
* During gapless transitions, there may be a brief period (a few audio buffers, ~10-50ms)
* where new track samples are processed with the previous track's gain before the update
* takes effect. This is a fundamental limitation of the async nature of Player.Listener
* callbacks vs continuous audio rendering. In practice, this is imperceptible compared
* to the original issue where the delay was ~500ms.
*/
class ReplayGainAudioProcessor(var mode: ReplayGainMode, var preAmpGain: Double = 0.0) : BaseAudioProcessor() {
@Volatile
var trackGain: Double? = null
@Synchronized get

@Synchronized set

@Volatile
var albumGain: Double? = null
@Synchronized get

@Synchronized set

private val gain: Double
get() =
@Synchronized get() =
preAmpGain +
when (mode) {
ReplayGainMode.Track -> trackGain ?: albumGain ?: 0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ class ExoPlayerPlayback(
val transitionReason = reason.toTransitionReason()
Timber.v("onMediaItemTransition(reason: ${reason.toTransitionReason()})")

// Update ReplayGain immediately from the MediaItem's tag to ensure correct gain for this track
mediaItem?.localConfiguration?.tag?.let { tag ->
if (tag is ReplayGainTag) {
Timber.v("Updating ReplayGain from MediaItem tag: track=${tag.trackGain}, album=${tag.albumGain}")
replayGainAudioProcessor.trackGain = tag.trackGain
replayGainAudioProcessor.albumGain = tag.albumGain
}
}

when (transitionReason) {
TransitionReason.Repeat -> callback?.onTrackEnded(true)
TransitionReason.Auto -> callback?.onTrackEnded(true)
Expand Down Expand Up @@ -150,7 +159,11 @@ class ExoPlayerPlayback(

val mediaInfo = mediaInfoProvider.getMediaInfo(current)
player.addListener(eventListener)
player.setMediaItem(getMediaItem(mediaInfo))
val mediaItem = getMediaItem(mediaInfo, current.replayGainTrack, current.replayGainAlbum)
player.setMediaItem(mediaItem)
// Set ReplayGain immediately for the current track in case onMediaItemTransition doesn't fire
replayGainAudioProcessor.trackGain = current.replayGainTrack
replayGainAudioProcessor.albumGain = current.replayGainAlbum
player.seekTo(seekPosition.toLong())
player.prepare()

Expand All @@ -174,7 +187,7 @@ class ExoPlayerPlayback(

val nextMediaItem: MediaItem? =
song?.let {
getMediaItem(mediaInfoProvider.getMediaInfo(song))
getMediaItem(mediaInfoProvider.getMediaInfo(song), song.replayGainTrack, song.replayGainAlbum)
}

val count = player.mediaItemCount
Expand Down Expand Up @@ -293,9 +306,16 @@ class ExoPlayerPlayback(
else -> TransitionReason.Unknown
}

data class ReplayGainTag(val trackGain: Double?, val albumGain: Double?)

@Throws(IllegalStateException::class)
fun getMediaItem(mediaInfo: MediaInfo): MediaItem = MediaItem.Builder()
fun getMediaItem(
mediaInfo: MediaInfo,
trackGain: Double? = null,
albumGain: Double? = null
): MediaItem = MediaItem.Builder()
.setMimeType(mediaInfo.mimeType)
.setUri(mediaInfo.path)
.setTag(ReplayGainTag(trackGain, albumGain))
.build()
}
Loading