playback: do not dynamically extract adjustments
Do not extract ReplayGain adjustments on the fly, instead doing them as we go along. This prevents an issue where the ReplayGain information would only be applied a short period after playback start, which is heavily jarring.
This commit is contained in:
parent
5ab46ba5d1
commit
bce03a5833
10 changed files with 113 additions and 125 deletions
|
@ -34,6 +34,7 @@ import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.info.Disc
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
import org.oxycblt.auxio.music.info.Name
|
import org.oxycblt.auxio.music.info.Name
|
||||||
import org.oxycblt.auxio.music.info.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||||
import org.oxycblt.auxio.util.concatLocalized
|
import org.oxycblt.auxio.util.concatLocalized
|
||||||
import org.oxycblt.auxio.util.toUuidOrNull
|
import org.oxycblt.auxio.util.toUuidOrNull
|
||||||
|
|
||||||
|
@ -255,6 +256,8 @@ interface Song : Music {
|
||||||
val size: Long
|
val size: Long
|
||||||
/** The duration of the audio file, in milliseconds. */
|
/** The duration of the audio file, in milliseconds. */
|
||||||
val durationMs: Long
|
val durationMs: Long
|
||||||
|
/** The ReplayGain adjustment to apply during playback. */
|
||||||
|
val replayGainAdjustment: ReplayGainAdjustment?
|
||||||
/** The date the audio file was added to the device, as a unix epoch timestamp. */
|
/** The date the audio file was added to the device, as a unix epoch timestamp. */
|
||||||
val dateAdded: Long
|
val dateAdded: Long
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -32,19 +32,19 @@ import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.metadata.correctWhitespace
|
import org.oxycblt.auxio.music.metadata.correctWhitespace
|
||||||
import org.oxycblt.auxio.music.metadata.splitEscaped
|
import org.oxycblt.auxio.music.metadata.splitEscaped
|
||||||
|
|
||||||
@Database(entities = [CachedSong::class], version = 27, exportSchema = false)
|
@Database(entities = [CachedSong::class], version = 32, exportSchema = false)
|
||||||
abstract class CacheDatabase : RoomDatabase() {
|
abstract class CacheDatabase : RoomDatabase() {
|
||||||
abstract fun cachedSongsDao(): CachedSongsDao
|
abstract fun cachedSongsDao(): CachedSongsDao
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface CachedSongsDao {
|
interface CachedSongsDao {
|
||||||
@Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readSongs(): List<CachedSong>
|
@Query("SELECT * FROM CachedSong") suspend fun readSongs(): List<CachedSong>
|
||||||
@Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeSongs()
|
@Query("DELETE FROM CachedSong") suspend fun nukeSongs()
|
||||||
@Insert suspend fun insertSongs(songs: List<CachedSong>)
|
@Insert suspend fun insertSongs(songs: List<CachedSong>)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity(tableName = CachedSong.TABLE_NAME)
|
@Entity
|
||||||
@TypeConverters(CachedSong.Converters::class)
|
@TypeConverters(CachedSong.Converters::class)
|
||||||
data class CachedSong(
|
data class CachedSong(
|
||||||
/**
|
/**
|
||||||
|
@ -60,6 +60,10 @@ data class CachedSong(
|
||||||
var size: Long? = null,
|
var size: Long? = null,
|
||||||
/** @see RawSong */
|
/** @see RawSong */
|
||||||
var durationMs: Long,
|
var durationMs: Long,
|
||||||
|
/** @see RawSong.replayGainTrackAdjustment */
|
||||||
|
val replayGainTrackAdjustment: Float?,
|
||||||
|
/** @see RawSong.replayGainAlbumAdjustment */
|
||||||
|
val replayGainAlbumAdjustment: Float?,
|
||||||
/** @see RawSong.musicBrainzId */
|
/** @see RawSong.musicBrainzId */
|
||||||
var musicBrainzId: String? = null,
|
var musicBrainzId: String? = null,
|
||||||
/** @see RawSong.name */
|
/** @see RawSong.name */
|
||||||
|
@ -97,7 +101,7 @@ data class CachedSong(
|
||||||
/** @see RawSong.genreNames */
|
/** @see RawSong.genreNames */
|
||||||
var genreNames: List<String> = listOf()
|
var genreNames: List<String> = listOf()
|
||||||
) {
|
) {
|
||||||
fun copyToRaw(rawSong: RawSong): CachedSong {
|
fun copyToRaw(rawSong: RawSong) {
|
||||||
rawSong.musicBrainzId = musicBrainzId
|
rawSong.musicBrainzId = musicBrainzId
|
||||||
rawSong.name = name
|
rawSong.name = name
|
||||||
rawSong.sortName = sortName
|
rawSong.sortName = sortName
|
||||||
|
@ -105,6 +109,9 @@ data class CachedSong(
|
||||||
rawSong.size = size
|
rawSong.size = size
|
||||||
rawSong.durationMs = durationMs
|
rawSong.durationMs = durationMs
|
||||||
|
|
||||||
|
rawSong.replayGainTrackAdjustment = replayGainTrackAdjustment
|
||||||
|
rawSong.replayGainAlbumAdjustment = replayGainAlbumAdjustment
|
||||||
|
|
||||||
rawSong.track = track
|
rawSong.track = track
|
||||||
rawSong.disc = disc
|
rawSong.disc = disc
|
||||||
rawSong.subtitle = subtitle
|
rawSong.subtitle = subtitle
|
||||||
|
@ -124,7 +131,6 @@ data class CachedSong(
|
||||||
rawSong.albumArtistSortNames = albumArtistSortNames
|
rawSong.albumArtistSortNames = albumArtistSortNames
|
||||||
|
|
||||||
rawSong.genreNames = genreNames
|
rawSong.genreNames = genreNames
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
|
@ -141,8 +147,6 @@ data class CachedSong(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TABLE_NAME = "cached_songs"
|
|
||||||
|
|
||||||
fun fromRaw(rawSong: RawSong) =
|
fun fromRaw(rawSong: RawSong) =
|
||||||
CachedSong(
|
CachedSong(
|
||||||
mediaStoreId =
|
mediaStoreId =
|
||||||
|
@ -155,6 +159,8 @@ data class CachedSong(
|
||||||
sortName = rawSong.sortName,
|
sortName = rawSong.sortName,
|
||||||
size = rawSong.size,
|
size = rawSong.size,
|
||||||
durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" },
|
durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" },
|
||||||
|
replayGainTrackAdjustment = rawSong.replayGainTrackAdjustment,
|
||||||
|
replayGainAlbumAdjustment = rawSong.replayGainAlbumAdjustment,
|
||||||
track = rawSong.track,
|
track = rawSong.track,
|
||||||
disc = rawSong.disc,
|
disc = rawSong.disc,
|
||||||
subtitle = rawSong.subtitle,
|
subtitle = rawSong.subtitle,
|
||||||
|
|
|
@ -43,7 +43,6 @@ class CacheRoomModule {
|
||||||
Room.databaseBuilder(
|
Room.databaseBuilder(
|
||||||
context.applicationContext, CacheDatabase::class.java, "music_cache.db")
|
context.applicationContext, CacheDatabase::class.java, "music_cache.db")
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.fallbackToDestructiveMigrationFrom(0)
|
|
||||||
.fallbackToDestructiveMigrationOnDowngrade()
|
.fallbackToDestructiveMigrationOnDowngrade()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,8 @@ import org.oxycblt.auxio.music.info.Name
|
||||||
import org.oxycblt.auxio.music.info.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
||||||
import org.oxycblt.auxio.music.metadata.parseMultiValue
|
import org.oxycblt.auxio.music.metadata.parseMultiValue
|
||||||
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import org.oxycblt.auxio.util.toUuidOrNull
|
import org.oxycblt.auxio.util.toUuidOrNull
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
@ -88,6 +90,18 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
fromFormat = null)
|
fromFormat = null)
|
||||||
override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" }
|
override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" }
|
||||||
override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }
|
override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }
|
||||||
|
override val replayGainAdjustment =
|
||||||
|
if (rawSong.replayGainTrackAdjustment != null &&
|
||||||
|
rawSong.replayGainAlbumAdjustment != null) {
|
||||||
|
ReplayGainAdjustment(
|
||||||
|
track = unlikelyToBeNull(rawSong.replayGainTrackAdjustment),
|
||||||
|
album = unlikelyToBeNull(rawSong.replayGainAlbumAdjustment))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
.also {
|
||||||
|
logD("${rawSong.replayGainTrackAdjustment} ${rawSong.replayGainAlbumAdjustment}}")
|
||||||
|
}
|
||||||
override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" }
|
override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" }
|
||||||
private var _album: AlbumImpl? = null
|
private var _album: AlbumImpl? = null
|
||||||
override val album: Album
|
override val album: Album
|
||||||
|
|
|
@ -51,6 +51,10 @@ data class RawSong(
|
||||||
var durationMs: Long? = null,
|
var durationMs: Long? = null,
|
||||||
/** @see Song.mimeType */
|
/** @see Song.mimeType */
|
||||||
var extensionMimeType: String? = null,
|
var extensionMimeType: String? = null,
|
||||||
|
/** @see Song.replayGainAdjustment */
|
||||||
|
var replayGainTrackAdjustment: Float? = null,
|
||||||
|
/** @see Song.replayGainAdjustment */
|
||||||
|
var replayGainAlbumAdjustment: Float? = null,
|
||||||
/** @see Music.UID */
|
/** @see Music.UID */
|
||||||
var musicBrainzId: String? = null,
|
var musicBrainzId: String? = null,
|
||||||
/** @see Music.name */
|
/** @see Music.name */
|
||||||
|
|
|
@ -174,6 +174,13 @@ private class TagWorkerImpl(
|
||||||
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
||||||
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
||||||
}
|
}
|
||||||
|
// ReplayGain information
|
||||||
|
textFrames["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()?.let {
|
||||||
|
rawSong.replayGainTrackAdjustment = it
|
||||||
|
}
|
||||||
|
textFrames["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()?.let {
|
||||||
|
rawSong.replayGainAlbumAdjustment = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
|
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
|
||||||
|
@ -271,10 +278,38 @@ private class TagWorkerImpl(
|
||||||
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
||||||
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReplayGain information
|
||||||
|
// Most ReplayGain tags are formatted as a simple decibel adjustment in a custom
|
||||||
|
// replaygain_*_gain tag, but opus has it's own "r128_*_gain" ReplayGain specification,
|
||||||
|
// which requires dividing the adjustment by 256 to get the gain. This is used alongside
|
||||||
|
// the base adjustment intrinsic to the format to create the normalized adjustment. This is
|
||||||
|
// normally the only tag used for opus files, but some software still writes replay gain
|
||||||
|
// tags anyway.
|
||||||
|
(comments["r128_track_gain"]?.parseReplayGainAdjustment()?.div(256)
|
||||||
|
?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment())
|
||||||
|
?.let { rawSong.replayGainTrackAdjustment = it }
|
||||||
|
(comments["r128_album_gain"]?.parseReplayGainAdjustment()?.div(256)
|
||||||
|
?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment())
|
||||||
|
?.let { rawSong.replayGainAlbumAdjustment = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a ReplayGain adjustment into a float value.
|
||||||
|
*
|
||||||
|
* @return A parsed adjustment float, or null if the adjustment had invalid formatting.
|
||||||
|
*/
|
||||||
|
private fun List<String>.parseReplayGainAdjustment() =
|
||||||
|
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists")
|
val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists")
|
||||||
val COMPILATION_RELEASE_TYPES = listOf("compilation")
|
val COMPILATION_RELEASE_TYPES = listOf("compilation")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
|
||||||
|
* https://github.com/vanilla-music/vanilla
|
||||||
|
*/
|
||||||
|
val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,14 @@ enum class ReplayGainMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a ReplayGain adjustment to apply during song playback.
|
||||||
|
*
|
||||||
|
* @param track The track-specific adjustment that should be applied.
|
||||||
|
* @param album A more general album-specific adjustment that should be applied.
|
||||||
|
*/
|
||||||
|
data class ReplayGainAdjustment(val track: Float, val album: Float)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current ReplayGain pre-amp configuration.
|
* The current ReplayGain pre-amp configuration.
|
||||||
*
|
*
|
||||||
|
|
|
@ -21,15 +21,16 @@ package org.oxycblt.auxio.playback.replaygain
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Tracks
|
|
||||||
import androidx.media3.common.audio.AudioProcessor
|
import androidx.media3.common.audio.AudioProcessor
|
||||||
import androidx.media3.exoplayer.audio.BaseAudioProcessor
|
import androidx.media3.exoplayer.audio.BaseAudioProcessor
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.metadata.TextTags
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
|
import org.oxycblt.auxio.playback.queue.Queue
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -48,9 +49,7 @@ class ReplayGainAudioProcessor
|
||||||
constructor(
|
constructor(
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val playbackSettings: PlaybackSettings
|
private val playbackSettings: PlaybackSettings
|
||||||
) : BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener {
|
) : BaseAudioProcessor(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
||||||
private var lastFormat: Format? = null
|
|
||||||
|
|
||||||
private var volume = 1f
|
private var volume = 1f
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
@ -58,51 +57,38 @@ constructor(
|
||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
init {
|
||||||
* Add this instance to the components required for it to function correctly.
|
playbackManager.addListener(this)
|
||||||
*
|
|
||||||
* @param player The [Player] to attach to. Should already have this instance as an audio
|
|
||||||
* processor.
|
|
||||||
*/
|
|
||||||
fun addToListeners(player: Player) {
|
|
||||||
player.addListener(this)
|
|
||||||
playbackSettings.registerListener(this)
|
playbackSettings.registerListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Remove this instance from the components required for it to function correctly. */
|
||||||
* Remove this instance from the components required for it to function correctly.
|
fun release() {
|
||||||
*
|
playbackManager.removeListener(this)
|
||||||
* @param player The [Player] to detach from. Should already have this instance as an audio
|
|
||||||
* processor.
|
|
||||||
*/
|
|
||||||
fun releaseFromListeners(player: Player) {
|
|
||||||
player.removeListener(this)
|
|
||||||
playbackSettings.unregisterListener(this)
|
playbackSettings.unregisterListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- OVERRIDES ---
|
// --- OVERRIDES ---
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onIndexMoved(queue: Queue) {
|
||||||
super.onTracksChanged(tracks)
|
logD("Index moved, updating current song")
|
||||||
// Try to find the currently playing track so we can update the ReplayGain adjustment
|
applyReplayGain(queue.currentSong)
|
||||||
// based on it.
|
}
|
||||||
for (group in tracks.groups) {
|
|
||||||
if (group.isSelected) {
|
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
||||||
for (i in 0 until group.length) {
|
// Other types of queue changes preserve the current song.
|
||||||
if (group.isTrackSelected(i)) {
|
if (change.type == Queue.Change.Type.SONG) {
|
||||||
applyReplayGain(group.getTrackFormat(i))
|
applyReplayGain(queue.currentSong)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||||
}
|
logD("New playback started, updating playback information")
|
||||||
// Nothing selected, apply nothing
|
applyReplayGain(queue.currentSong)
|
||||||
applyReplayGain(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReplayGainSettingsChanged() {
|
override fun onReplayGainSettingsChanged() {
|
||||||
// ReplayGain config changed, we need to set it up again.
|
// ReplayGain config changed, we need to set it up again.
|
||||||
applyReplayGain(lastFormat)
|
applyReplayGain(playbackManager.queue.currentSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REPLAYGAIN PARSING ---
|
// --- REPLAYGAIN PARSING ---
|
||||||
|
@ -110,11 +96,11 @@ constructor(
|
||||||
/**
|
/**
|
||||||
* Updates the volume adjustment based on the given [Format].
|
* Updates the volume adjustment based on the given [Format].
|
||||||
*
|
*
|
||||||
* @param format The [Format] of the currently playing track, or null if nothing is playing.
|
* @param song The [Format] of the currently playing track, or null if nothing is playing.
|
||||||
*/
|
*/
|
||||||
private fun applyReplayGain(format: Format?) {
|
private fun applyReplayGain(song: Song?) {
|
||||||
lastFormat = format
|
logD("Applying ReplayGain adjustment for $song")
|
||||||
val gain = parseReplayGain(format ?: return)
|
val gain = song?.replayGainAdjustment
|
||||||
val preAmp = playbackSettings.replayGainPreAmp
|
val preAmp = playbackSettings.replayGainPreAmp
|
||||||
|
|
||||||
val adjust =
|
val adjust =
|
||||||
|
@ -167,58 +153,6 @@ constructor(
|
||||||
volume = 10f.pow(adjust / 20f)
|
volume = 10f.pow(adjust / 20f)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse ReplayGain information from the given [Format].
|
|
||||||
*
|
|
||||||
* @param format The [Format] to parse.
|
|
||||||
* @return A [Adjustment] adjustment, or null if there were no valid adjustments.
|
|
||||||
*/
|
|
||||||
private fun parseReplayGain(format: Format): Adjustment? {
|
|
||||||
val textTags = TextTags(format.metadata ?: return null)
|
|
||||||
var trackGain = 0f
|
|
||||||
var albumGain = 0f
|
|
||||||
|
|
||||||
// Most ReplayGain tags are formatted as a simple decibel adjustment in a custom
|
|
||||||
// replaygain_*_gain tag.
|
|
||||||
textTags.id3v2["TXXX:$TAG_RG_TRACK_GAIN"]
|
|
||||||
?.run { first().parseReplayGainAdjustment() }
|
|
||||||
?.let { trackGain = it }
|
|
||||||
textTags.id3v2["TXXX:$TAG_RG_ALBUM_GAIN"]
|
|
||||||
?.run { first().parseReplayGainAdjustment() }
|
|
||||||
?.let { albumGain = it }
|
|
||||||
textTags.vorbis[TAG_RG_ALBUM_GAIN]
|
|
||||||
?.run { first().parseReplayGainAdjustment() }
|
|
||||||
?.let { trackGain = it }
|
|
||||||
textTags.vorbis[TAG_RG_TRACK_GAIN]
|
|
||||||
?.run { first().parseReplayGainAdjustment() }
|
|
||||||
?.let { albumGain = it }
|
|
||||||
|
|
||||||
// Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the
|
|
||||||
// adjustment by 256 to get the gain. This is used alongside the base adjustment
|
|
||||||
// intrinsic to the format to create the normalized adjustment. This is normally the only
|
|
||||||
// tag used for opus files, but some software still writes replay gain tags anyway.
|
|
||||||
textTags.vorbis[TAG_R128_TRACK_GAIN]
|
|
||||||
?.run { first().parseReplayGainAdjustment() }
|
|
||||||
?.let { trackGain = it / 256f }
|
|
||||||
textTags.vorbis[TAG_R128_ALBUM_GAIN]
|
|
||||||
?.run { first().parseReplayGainAdjustment() }
|
|
||||||
?.let { albumGain = it / 256f }
|
|
||||||
|
|
||||||
return if (trackGain != 0f || albumGain != 0f) {
|
|
||||||
Adjustment(trackGain, albumGain)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a ReplayGain adjustment into a float value.
|
|
||||||
*
|
|
||||||
* @return A parsed adjustment float, or null if the adjustment had invalid formatting.
|
|
||||||
*/
|
|
||||||
private fun String.parseReplayGainAdjustment() =
|
|
||||||
replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()
|
|
||||||
|
|
||||||
// --- AUDIO PROCESSOR IMPLEMENTATION ---
|
// --- AUDIO PROCESSOR IMPLEMENTATION ---
|
||||||
|
|
||||||
override fun onConfigure(
|
override fun onConfigure(
|
||||||
|
@ -284,25 +218,4 @@ constructor(
|
||||||
put(short.toByte())
|
put(short.toByte())
|
||||||
put(short.toInt().shr(8).toByte())
|
put(short.toInt().shr(8).toByte())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The resolved ReplayGain adjustment for a file.
|
|
||||||
*
|
|
||||||
* @param track The track adjustment (in dB), or 0 if it is not present.
|
|
||||||
* @param album The album adjustment (in dB), or 0 if it is not present.
|
|
||||||
*/
|
|
||||||
private data class Adjustment(val track: Float, val album: Float)
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val TAG_RG_TRACK_GAIN = "replaygain_track_gain"
|
|
||||||
const val TAG_RG_ALBUM_GAIN = "replaygain_album_gain"
|
|
||||||
const val TAG_R128_TRACK_GAIN = "r128_track_gain"
|
|
||||||
const val TAG_R128_ALBUM_GAIN = "r128_album_gain"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
|
|
||||||
* https://github.com/vanilla-music/vanilla
|
|
||||||
*/
|
|
||||||
val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,6 @@ class PlaybackService :
|
||||||
true)
|
true)
|
||||||
.build()
|
.build()
|
||||||
.also { it.addListener(this) }
|
.also { it.addListener(this) }
|
||||||
replayGainProcessor.addToListeners(player)
|
|
||||||
foregroundManager = ForegroundManager(this)
|
foregroundManager = ForegroundManager(this)
|
||||||
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
||||||
// condition to cause us to load music before we were fully initialize.
|
// condition to cause us to load music before we were fully initialize.
|
||||||
|
@ -196,7 +195,7 @@ class PlaybackService :
|
||||||
widgetComponent.release()
|
widgetComponent.release()
|
||||||
mediaSessionComponent.release()
|
mediaSessionComponent.release()
|
||||||
|
|
||||||
replayGainProcessor.releaseFromListeners(player)
|
replayGainProcessor.release()
|
||||||
player.release()
|
player.release()
|
||||||
if (openAudioEffectSession) {
|
if (openAudioEffectSession) {
|
||||||
// Make sure to close the audio session when we release the player.
|
// Make sure to close the audio session when we release the player.
|
||||||
|
|
|
@ -50,6 +50,13 @@ fun Int.nonZeroOrNull() = if (this > 0) this else null
|
||||||
*/
|
*/
|
||||||
fun Long.nonZeroOrNull() = if (this > 0) this else null
|
fun Long.nonZeroOrNull() = if (this > 0) this else null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aliases a check to ensure that the given number is non-zero.
|
||||||
|
*
|
||||||
|
* @return The same number if it's non-zero, null otherwise.
|
||||||
|
*/
|
||||||
|
fun Float.nonZeroOrNull() = if (this > 0) this else null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aliases a check to ensure a given value is in a specified range.
|
* Aliases a check to ensure a given value is in a specified range.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue