diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index ef515357f..fa263f054 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -104,9 +104,9 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment) { val mimeTypes = mutableSetOf() for (song in songs) { builder.addStream(song.uri) - mimeTypes.add(song.mimeType.fromFormat ?: song.mimeType.fromExtension) + mimeTypes.add(song.format.mimeType) } builder.setType(mimeTypes.singleOrNull() ?: "audio/*").startChooser() diff --git a/app/src/main/java/org/oxycblt/musikr/Music.kt b/app/src/main/java/org/oxycblt/musikr/Music.kt index 6cc2405f5..c130514a4 100644 --- a/app/src/main/java/org/oxycblt/musikr/Music.kt +++ b/app/src/main/java/org/oxycblt/musikr/Music.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.fs.MimeType +import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Disc @@ -252,8 +252,8 @@ interface Song : Music { * instead for accessing the audio file. */ val path: Path - /** The [MimeType] of the audio file. Only intended for display. */ - val mimeType: MimeType + /** The [Format] of the audio file. Only intended for display. */ + val format: Format /** The size of the audio file, in bytes. */ val size: Long /** The duration of the audio file, in milliseconds. */ diff --git a/app/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/app/src/main/java/org/oxycblt/musikr/cache/Cache.kt index 25b5fb4a2..9d58fd519 100644 --- a/app/src/main/java/org/oxycblt/musikr/cache/Cache.kt +++ b/app/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -18,6 +18,7 @@ package org.oxycblt.musikr.cache +import org.oxycblt.ktaglib.Properties import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.tag.parse.ParsedTags @@ -34,7 +35,11 @@ interface Cache { } } -data class CachedSong(val parsedTags: ParsedTags, val cover: Cover.Single?) +data class CachedSong( + val parsedTags: ParsedTags, + val cover: Cover.Single?, + val properties: Properties +) private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache { override suspend fun read(file: DeviceFile) = diff --git a/app/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index 95481873e..9e9693b13 100644 --- a/app/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -30,6 +30,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverter import androidx.room.TypeConverters +import org.oxycblt.ktaglib.Properties import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.tag.Date @@ -67,7 +68,6 @@ internal data class CachedInfo( */ @PrimaryKey val uri: String, val dateModified: Long, - val durationMs: Long, val replayGainTrackAdjustment: Float?, val replayGainAlbumAdjustment: Float?, val musicBrainzId: String?, @@ -88,7 +88,11 @@ internal data class CachedInfo( val albumArtistNames: List, val albumArtistSortNames: List, val genreNames: List, - val cover: Cover.Single? = null + val cover: Cover.Single?, + val mimeType: String, + val durationMs: Long, + val bitrate: Int, + val sampleRate: Int, ) { fun intoCachedSong() = CachedSong( @@ -114,7 +118,8 @@ internal data class CachedInfo( albumArtistNames = albumArtistNames, albumArtistSortNames = albumArtistSortNames, genreNames = genreNames), - cover) + cover, + Properties(mimeType, durationMs, bitrate, sampleRate)) object Converters { @TypeConverter @@ -159,6 +164,9 @@ internal data class CachedInfo( albumArtistNames = cachedSong.parsedTags.albumArtistNames, albumArtistSortNames = cachedSong.parsedTags.albumArtistSortNames, genreNames = cachedSong.parsedTags.genreNames, - cover = cachedSong.cover) + cover = cachedSong.cover, + mimeType = cachedSong.properties.mimeType, + bitrate = cachedSong.properties.bitrate, + sampleRate = cachedSong.properties.sampleRate) } } diff --git a/app/src/main/java/org/oxycblt/musikr/fs/Format.kt b/app/src/main/java/org/oxycblt/musikr/fs/Format.kt new file mode 100644 index 000000000..d4172238e --- /dev/null +++ b/app/src/main/java/org/oxycblt/musikr/fs/Format.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024 Auxio Project + * Format.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.fs + +import android.webkit.MimeTypeMap +import org.oxycblt.auxio.util.unlikelyToBeNull + +sealed interface Format { + val mimeType: String + + data object MPEG3 : Format { + override val mimeType = "audio/mpeg" + } + + data class MPEG4(val containing: Format?) : Format { + override val mimeType = "audio/mp4" + } + + data object AAC : Format { + override val mimeType = "audio/aac" + } + + data object ALAC : Format { + override val mimeType = "audio/alac" + } + + data class Ogg(val containing: Format?) : Format { + override val mimeType = "audio/ogg" + } + + data object Opus : Format { + override val mimeType = "audio/opus" + } + + data object Vorbis : Format { + override val mimeType = "audio/vorbis" + } + + data object FLAC : Format { + override val mimeType = "audio/flac" + } + + data object Wav : Format { + override val mimeType = "audio/wav" + } + + data class Unknown(override val mimeType: String) : Format { + val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.uppercase() + } + + companion object { + private val CODEC_MAP = + mapOf( + "audio/mpeg" to MPEG3, + "audio/mp3" to MPEG3, + "audio/aac" to AAC, + "audio/aacp" to AAC, + "audio/3gpp" to AAC, + "audio/3gpp2" to AAC, + "audio/alac" to ALAC, + "audio/opus" to Opus, + "audio/vorbis" to Vorbis, + "audio/flac" to FLAC, + "audio/wav" to Wav, + "audio/raw" to Wav, + "audio/x-wav" to Wav, + "audio/vnd.wave" to Wav, + "audio/wave" to Wav, + ) + + fun infer(containerMimeType: String, codecMimeType: String): Format { + val codecFormat = CODEC_MAP[codecMimeType] + if (codecFormat != null) { + // Codec found, possibly wrap in container. + return unlikelyToBeNull(wrapInContainer(containerMimeType, codecFormat)) + } + val extensionFormat = CODEC_MAP[containerMimeType] + if (extensionFormat != null) { + // Standalone container of some codec. + return extensionFormat + } + return wrapInContainer(containerMimeType, null) ?: Unknown(containerMimeType) + } + + private fun wrapInContainer(containerMimeType: String, format: Format?) = + when (containerMimeType) { + "audio/mp4", + "audio/mp4a-latm", + "audio/mpeg4-generic" -> MPEG4(format) + "audio/ogg", + "application/ogg", + "application/x-ogg" -> Ogg(format) + else -> format + } + } +} diff --git a/app/src/main/java/org/oxycblt/musikr/fs/MimeType.kt b/app/src/main/java/org/oxycblt/musikr/fs/MimeType.kt deleted file mode 100644 index 563f692ca..000000000 --- a/app/src/main/java/org/oxycblt/musikr/fs/MimeType.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * MimeType.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.musikr.fs - -import android.content.Context -import android.media.MediaFormat -import android.webkit.MimeTypeMap -import org.oxycblt.auxio.R - -/** - * A mime type of a file. Only intended for display. - * - * @param fromExtension The mime type obtained by analyzing the file extension. - * @param fromFormat The mime type obtained by analyzing the file format. Null if could not be - * obtained. - * @author Alexander Capehart (OxygenCobalt) - * - * TODO: Get around to simplifying this - */ -data class MimeType(val fromExtension: String, val fromFormat: String?) { - /** - * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis". - * - * @param context [Context] required to obtain human-readable strings. - * @return A human-readable name for this mime type. Will first try [fromFormat], then falling - * back to [fromExtension], and then null if that fails. - */ - fun resolveName(context: Context): String? { - // We try our best to produce a more readable name for the common audio formats. - val formatName = - when (fromFormat) { - // We start with the extracted mime types, as they are more consistent. Note that - // we do not include container formats at all with these names. It is only the - // inner codec that we bother with. - MediaFormat.MIMETYPE_AUDIO_MPEG -> R.string.cdc_mp3 - MediaFormat.MIMETYPE_AUDIO_AAC -> R.string.cdc_aac - MediaFormat.MIMETYPE_AUDIO_VORBIS -> R.string.cdc_vorbis - MediaFormat.MIMETYPE_AUDIO_OPUS -> R.string.cdc_opus - MediaFormat.MIMETYPE_AUDIO_FLAC -> R.string.cdc_flac - // TODO: Add ALAC to this as soon as I can stop using MediaFormat for - // extracting metadata and just use ExoPlayer. - // We don't give a name to more unpopular formats. - else -> -1 - } - - if (formatName > -1) { - return context.getString(formatName) - } - - // Fall back to the file extension in the case that we have no mime type or - // a useless "audio/raw" mime type. Here: - // - We return names for container formats instead of the inner format, as we - // cannot parse the file. - // - We are at the mercy of the Android OS, hence we check for every possible mime - // type for a particular format according to Wikipedia. - val extensionName = - when (fromExtension) { - "audio/mpeg", - "audio/mp3" -> R.string.cdc_mp3 - "audio/mp4", - "audio/mp4a-latm", - "audio/mpeg4-generic" -> R.string.cdc_mp4 - "audio/aac", - "audio/aacp", - "audio/3gpp", - "audio/3gpp2" -> R.string.cdc_aac - "audio/ogg", - "application/ogg", - "application/x-ogg" -> R.string.cdc_ogg - "audio/flac" -> R.string.cdc_flac - "audio/wav", - "audio/x-wav", - "audio/wave", - "audio/vnd.wave" -> R.string.cdc_wav - "audio/x-matroska" -> R.string.cdc_mka - else -> -1 - } - - return if (extensionName > -1) { - context.getString(extensionName) - } else { - // Fall back to the extension if we can't find a special name for this format. - MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase() - } - } -} diff --git a/app/src/main/java/org/oxycblt/musikr/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/musikr/metadata/AudioProperties.kt index cec4a637d..4445fe99a 100644 --- a/app/src/main/java/org/oxycblt/musikr/metadata/AudioProperties.kt +++ b/app/src/main/java/org/oxycblt/musikr/metadata/AudioProperties.kt @@ -24,7 +24,6 @@ import android.media.MediaFormat import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.musikr.Song -import org.oxycblt.musikr.fs.MimeType import timber.log.Timber as L /** @@ -32,14 +31,9 @@ import timber.log.Timber as L * * @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed. * @param sampleRateHz The sample rate, in hertz. - * @param resolvedMimeType The known mime type of the [Song] after it's file format was determined. * @author Alexander Capehart (OxygenCobalt) */ -data class AudioProperties( - val bitrateKbps: Int?, - val sampleRateHz: Int?, - val resolvedMimeType: MimeType -) { +data class AudioProperties(val bitrateKbps: Int?, val sampleRateHz: Int?) { /** Implements the process of extracting [AudioProperties] from a given [Song]. */ interface Factory { /** @@ -75,7 +69,7 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. // that we can show. L.w("Unable to extract song attributes.") L.w(e.stackTraceToString()) - return AudioProperties(null, null, song.mimeType) + return AudioProperties(null, null) } // Get the first track from the extractor (This is basically always the only @@ -102,23 +96,10 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. null } - // The song's mime type won't have a populated format field right now, try to - // extract it ourselves. - val formatMimeType = - try { - format.getString(MediaFormat.KEY_MIME) - } catch (e: NullPointerException) { - L.e("Unable to extract mime type field") - null - } - extractor.release() L.d("Finished extracting audio properties") - return AudioProperties( - bitrate, - sampleRate, - MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType)) + return AudioProperties(bitrate, sampleRate) } } diff --git a/app/src/main/java/org/oxycblt/musikr/model/SongImpl.kt b/app/src/main/java/org/oxycblt/musikr/model/SongImpl.kt index 206a7fd3f..8a49d3dda 100644 --- a/app/src/main/java/org/oxycblt/musikr/model/SongImpl.kt +++ b/app/src/main/java/org/oxycblt/musikr/model/SongImpl.kt @@ -49,7 +49,7 @@ class SongImpl(private val handle: SongCore) : Song { override val date = preSong.date override val uri = preSong.uri override val path = preSong.path - override val mimeType = preSong.mimeType + override val format = preSong.format override val size = preSong.size override val durationMs = preSong.durationMs override val replayGainAdjustment = preSong.replayGainAdjustment diff --git a/app/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt b/app/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt index 97a356973..3892ae50b 100644 --- a/app/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt +++ b/app/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt @@ -53,7 +53,10 @@ private class EvaluateStepImpl( val preSongs = extractedMusic .filterIsInstance() - .map { tagInterpreter.interpret(it.file, it.tags, it.cover, interpretation) } + .map { + tagInterpreter.interpret( + it.file, it.tags, it.cover, it.properties, interpretation) + } .flowOn(Dispatchers.Main) .buffer(Channel.UNLIMITED) val graphBuilder = MusicGraph.builder() diff --git a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index e1fdad254..49cf46e6e 100644 --- a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge +import org.oxycblt.ktaglib.Properties import org.oxycblt.musikr.Storage import org.oxycblt.musikr.cache.CachedSong import org.oxycblt.musikr.cover.Cover @@ -63,7 +64,7 @@ private class ExtractStepImpl( val (cachedSongs, uncachedSongs) = cacheResults.mapPartition { it.cachedSong?.let { song -> - ExtractedMusic.Song(it.file, song.parsedTags, song.cover) + ExtractedMusic.Song(it.file, song.properties, song.parsedTags, song.cover) } } val split = uncachedSongs.distribute(16) @@ -73,10 +74,9 @@ private class ExtractStepImpl( .mapNotNull { node -> val metadata = metadataExtractor.extract(node.file) ?: return@mapNotNull null - L.d("Extracted tags for ${metadata.id3v2}") val tags = tagParser.parse(node.file, metadata) val cover = metadata.cover?.let { storage.storedCovers.write(it) } - ExtractedMusic.Song(node.file, tags, cover) + ExtractedMusic.Song(node.file, metadata.properties, tags, cover) } .flowOn(Dispatchers.IO) .buffer(Channel.UNLIMITED) @@ -84,7 +84,7 @@ private class ExtractStepImpl( val writtenSongs = merge(*extractedSongs) .map { - storage.cache.write(it.file, CachedSong(it.tags, it.cover)) + storage.cache.write(it.file, CachedSong(it.tags, it.cover, it.properties)) it } .flowOn(Dispatchers.IO) @@ -100,6 +100,10 @@ private class ExtractStepImpl( } sealed interface ExtractedMusic { - data class Song(val file: DeviceFile, val tags: ParsedTags, val cover: Cover.Single?) : - ExtractedMusic + data class Song( + val file: DeviceFile, + val properties: Properties, + val tags: ParsedTags, + val cover: Cover.Single? + ) : ExtractedMusic } diff --git a/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt b/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt index 0bc523afe..de5b8b77f 100644 --- a/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt +++ b/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt @@ -25,7 +25,7 @@ import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.update import org.oxycblt.musikr.Music import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.fs.MimeType +import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.playlist.PlaylistHandle import org.oxycblt.musikr.tag.Date @@ -42,7 +42,7 @@ data class PreSong( val date: Date?, val uri: Uri, val path: Path, - val mimeType: MimeType, + val format: Format, val size: Long, val durationMs: Long, val replayGainAdjustment: ReplayGainAdjustment, diff --git a/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt index 36ee911e3..b40f1268b 100644 --- a/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt +++ b/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt @@ -21,9 +21,10 @@ package org.oxycblt.musikr.tag.interpret import org.oxycblt.auxio.R import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.toUuidOrNull +import org.oxycblt.ktaglib.Properties import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.fs.MimeType +import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.tag.Disc import org.oxycblt.musikr.tag.Name @@ -36,6 +37,7 @@ interface TagInterpreter { file: DeviceFile, parsedTags: ParsedTags, cover: Cover.Single?, + properties: Properties, interpretation: Interpretation ): PreSong @@ -49,6 +51,7 @@ private data object TagInterpreterImpl : TagInterpreter { file: DeviceFile, parsedTags: ParsedTags, cover: Cover.Single?, + properties: Properties, interpretation: Interpretation ): PreSong { val individualPreArtists = @@ -79,7 +82,6 @@ private data object TagInterpreterImpl : TagInterpreter { date = parsedTags.date, uri = uri, path = file.path, - mimeType = MimeType(file.mimeType, null), size = file.size, durationMs = parsedTags.durationMs, replayGainAdjustment = @@ -87,6 +89,7 @@ private data object TagInterpreterImpl : TagInterpreter { parsedTags.replayGainTrackAdjustment, parsedTags.replayGainAlbumAdjustment, ), + format = Format.infer(file.mimeType, properties.mimeType), lastModified = file.lastModified, // TODO: Figure out what to do with date added dateAdded = file.lastModified,