diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index a9783cfae..edb5a69e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -34,6 +34,7 @@ import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Name 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.toUuidOrNull @@ -255,6 +256,8 @@ interface Song : Music { val size: Long /** The duration of the audio file, in milliseconds. */ 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. */ val dateAdded: Long /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt index 2cf6d33c0..7d1ac68d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt @@ -32,19 +32,19 @@ import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.metadata.correctWhitespace 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 fun cachedSongsDao(): CachedSongsDao } @Dao interface CachedSongsDao { - @Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readSongs(): List - @Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeSongs() + @Query("SELECT * FROM CachedSong") suspend fun readSongs(): List + @Query("DELETE FROM CachedSong") suspend fun nukeSongs() @Insert suspend fun insertSongs(songs: List) } -@Entity(tableName = CachedSong.TABLE_NAME) +@Entity @TypeConverters(CachedSong.Converters::class) data class CachedSong( /** @@ -60,6 +60,10 @@ data class CachedSong( var size: Long? = null, /** @see RawSong */ var durationMs: Long, + /** @see RawSong.replayGainTrackAdjustment */ + val replayGainTrackAdjustment: Float?, + /** @see RawSong.replayGainAlbumAdjustment */ + val replayGainAlbumAdjustment: Float?, /** @see RawSong.musicBrainzId */ var musicBrainzId: String? = null, /** @see RawSong.name */ @@ -97,7 +101,7 @@ data class CachedSong( /** @see RawSong.genreNames */ var genreNames: List = listOf() ) { - fun copyToRaw(rawSong: RawSong): CachedSong { + fun copyToRaw(rawSong: RawSong) { rawSong.musicBrainzId = musicBrainzId rawSong.name = name rawSong.sortName = sortName @@ -105,6 +109,9 @@ data class CachedSong( rawSong.size = size rawSong.durationMs = durationMs + rawSong.replayGainTrackAdjustment = replayGainTrackAdjustment + rawSong.replayGainAlbumAdjustment = replayGainAlbumAdjustment + rawSong.track = track rawSong.disc = disc rawSong.subtitle = subtitle @@ -124,7 +131,6 @@ data class CachedSong( rawSong.albumArtistSortNames = albumArtistSortNames rawSong.genreNames = genreNames - return this } object Converters { @@ -141,8 +147,6 @@ data class CachedSong( } companion object { - const val TABLE_NAME = "cached_songs" - fun fromRaw(rawSong: RawSong) = CachedSong( mediaStoreId = @@ -155,6 +159,8 @@ data class CachedSong( sortName = rawSong.sortName, size = rawSong.size, durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }, + replayGainTrackAdjustment = rawSong.replayGainTrackAdjustment, + replayGainAlbumAdjustment = rawSong.replayGainAlbumAdjustment, track = rawSong.track, disc = rawSong.disc, subtitle = rawSong.subtitle, diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt index 82e70f217..281cb6f4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt @@ -43,7 +43,6 @@ class CacheRoomModule { Room.databaseBuilder( context.applicationContext, CacheDatabase::class.java, "music_cache.db") .fallbackToDestructiveMigration() - .fallbackToDestructiveMigrationFrom(0) .fallbackToDestructiveMigrationOnDowngrade() .build() diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index e3ce99928..022d68c86 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -37,6 +37,8 @@ import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.metadata.parseId3GenreNames 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.toUuidOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -88,6 +90,18 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son fromFormat = null) override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" } 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" } private var _album: AlbumImpl? = null override val album: Album diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 46c84fc51..57539ec09 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -51,6 +51,10 @@ data class RawSong( var durationMs: Long? = null, /** @see Song.mimeType */ var extensionMimeType: String? = null, + /** @see Song.replayGainAdjustment */ + var replayGainTrackAdjustment: Float? = null, + /** @see Song.replayGainAdjustment */ + var replayGainAlbumAdjustment: Float? = null, /** @see Music.UID */ var musicBrainzId: String? = null, /** @see Music.name */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index e1501bede..691f8997a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -174,6 +174,13 @@ private class TagWorkerImpl( rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } 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>): Date? { @@ -271,10 +278,38 @@ private class TagWorkerImpl( rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } 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.parseReplayGainAdjustment() = + first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull() + private companion object { val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists") 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.-]") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt index 0b1855a50..8a85b73e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt @@ -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. * diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index ab86651e0..d74e45543 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -21,15 +21,16 @@ package org.oxycblt.auxio.playback.replaygain import androidx.media3.common.C import androidx.media3.common.Format import androidx.media3.common.Player -import androidx.media3.common.Tracks import androidx.media3.common.audio.AudioProcessor import androidx.media3.exoplayer.audio.BaseAudioProcessor import java.nio.ByteBuffer import javax.inject.Inject import kotlin.math.pow 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.queue.Queue import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.logD @@ -48,9 +49,7 @@ class ReplayGainAudioProcessor constructor( private val playbackManager: PlaybackStateManager, private val playbackSettings: PlaybackSettings -) : BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener { - private var lastFormat: Format? = null - +) : BaseAudioProcessor(), PlaybackStateManager.Listener, PlaybackSettings.Listener { private var volume = 1f set(value) { field = value @@ -58,51 +57,38 @@ constructor( flush() } - /** - * Add this instance to the components required for it to function correctly. - * - * @param player The [Player] to attach to. Should already have this instance as an audio - * processor. - */ - fun addToListeners(player: Player) { - player.addListener(this) + init { + playbackManager.addListener(this) playbackSettings.registerListener(this) } - /** - * Remove this instance from the components required for it to function correctly. - * - * @param player The [Player] to detach from. Should already have this instance as an audio - * processor. - */ - fun releaseFromListeners(player: Player) { - player.removeListener(this) + /** Remove this instance from the components required for it to function correctly. */ + fun release() { + playbackManager.removeListener(this) playbackSettings.unregisterListener(this) } // --- OVERRIDES --- - override fun onTracksChanged(tracks: Tracks) { - super.onTracksChanged(tracks) - // Try to find the currently playing track so we can update the ReplayGain adjustment - // based on it. - for (group in tracks.groups) { - if (group.isSelected) { - for (i in 0 until group.length) { - if (group.isTrackSelected(i)) { - applyReplayGain(group.getTrackFormat(i)) - return - } - } - } + override fun onIndexMoved(queue: Queue) { + logD("Index moved, updating current song") + applyReplayGain(queue.currentSong) + } + + override fun onQueueChanged(queue: Queue, change: Queue.Change) { + // Other types of queue changes preserve the current song. + if (change.type == Queue.Change.Type.SONG) { + applyReplayGain(queue.currentSong) } - // Nothing selected, apply nothing - applyReplayGain(null) + } + override fun onNewPlayback(queue: Queue, parent: MusicParent?) { + logD("New playback started, updating playback information") + applyReplayGain(queue.currentSong) } override fun onReplayGainSettingsChanged() { // ReplayGain config changed, we need to set it up again. - applyReplayGain(lastFormat) + applyReplayGain(playbackManager.queue.currentSong) } // --- REPLAYGAIN PARSING --- @@ -110,11 +96,11 @@ constructor( /** * 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?) { - lastFormat = format - val gain = parseReplayGain(format ?: return) + private fun applyReplayGain(song: Song?) { + logD("Applying ReplayGain adjustment for $song") + val gain = song?.replayGainAdjustment val preAmp = playbackSettings.replayGainPreAmp val adjust = @@ -167,58 +153,6 @@ constructor( 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 --- override fun onConfigure( @@ -284,25 +218,4 @@ constructor( put(short.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.-]") } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 848d47b4d..26629030b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -144,7 +144,6 @@ class PlaybackService : true) .build() .also { it.addListener(this) } - replayGainProcessor.addToListeners(player) foregroundManager = ForegroundManager(this) // 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. @@ -196,7 +195,7 @@ class PlaybackService : widgetComponent.release() mediaSessionComponent.release() - replayGainProcessor.releaseFromListeners(player) + replayGainProcessor.release() player.release() if (openAudioEffectSession) { // Make sure to close the audio session when we release the player. diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index 51835d68c..3ad2f8eb1 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -50,6 +50,13 @@ fun Int.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. *