diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 641c97e83..dc44181b0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -33,6 +33,7 @@ import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.MimeType import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.settings.SettingsManager @@ -51,7 +52,12 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * @author OxygenCobalt */ class DetailViewModel : ViewModel(), MusicStore.Callback { - data class DetailSong(val song: Song, val bitrateKbps: Int?, val sampleRate: Int?) + data class DetailSong( + val song: Song, + val bitrateKbps: Int?, + val sampleRate: Int?, + val resolvedMimeType: MimeType + ) private val musicStore = MusicStore.getInstance() private val settingsManager = SettingsManager.getInstance() @@ -157,7 +163,7 @@ class DetailViewModel : ViewModel(), MusicStore.Callback { } catch (e: Exception) { logW("Unable to extract song attributes.") logW(e.stackTraceToString()) - return@withContext DetailSong(song, null, null) + return@withContext DetailSong(song, null, null, song.mimeType) } val format = extractor.getTrackFormat(0) @@ -176,7 +182,24 @@ class DetailViewModel : ViewModel(), MusicStore.Callback { null } - DetailSong(song, bitrate, sampleRate) + val resolvedMimeType = + if (song.mimeType.fromFormat != null) { + // ExoPlayer was already able to populate the format. + song.mimeType + } else { + val formatMimeType = + try { + format.getString(MediaFormat.KEY_MIME) + } catch (e: Exception) { + null + } + + // Ensure that we don't include the functionally useless + // "audio/raw" mime type + MimeType(song.mimeType.fromExtension, formatMimeType) + } + + DetailSong(song, bitrate, sampleRate, resolvedMimeType) } } } @@ -188,7 +211,7 @@ class DetailViewModel : ViewModel(), MusicStore.Callback { // To create a good user experience regarding disc numbers, we intersperse // items that show the disc number throughout the album's songs. In the case - // that the album does not have distinct disc numbers, we omit the header. + // that the album does not have distinct disc numbers, we omit the header. val songs = albumSort.songs(album.songs) val byDisc = songs.groupBy { it.disc ?: 1 } if (byDisc.size > 1) { 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 743fb1f90..e6186b74b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -62,9 +62,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { binding.detailContainer.isGone = false binding.detailFileName.setText(song.song.path.name) binding.detailRelativeDir.setText(song.song.path.parent.resolveName(requireContext())) - binding.detailFormat.setText( - mimeTypes.getExtensionFromMimeType(song.song.mimeType)?.uppercase() - ?: getString(R.string.def_format)) + binding.detailFormat.setText(song.resolvedMimeType.resolveName(requireContext())) binding.detailSize.setText(Formatter.formatFileSize(requireContext(), song.song.size)) binding.detailDuration.setText(song.song.durationSecs.formatDuration(true)) @@ -85,6 +83,41 @@ class SongDetailDialog : ViewBindingDialogFragment() { } } + private fun getMimeName(mime: String): String? { + return when (mime) { + // Since Auxio only feasibly loads music, we can match for general mime types + // and assume that they are audio-based. + + // Classic formats + "audio/mpeg", + "audio/mp3" -> "MPEG-1 Layer 3" + "audio/ogg", + "application/ogg" -> "OGG" + "audio/vorbis" -> "OGG Vorbis" + "audio/opus" -> "OGG Opus" + "audio/flac" -> "(OGG) FLAC" + + // Modern formats + "audio/mp4", + "audio/mp4a-latm", + "audio/mpeg4-generic", + "audio/aac", + "audio/3gpp", + "audio/3gpp2", -> "Advanced Audio Coding (AAC)" + "audio/x-matroska" -> "Matroska Audio (MKA)" + + // Windows formats + "audio/wav", + "audio/x-wav", + "audio/wave", + "audio/vnd.wave" -> "Microsoft WAV" + "audio/x-ms-wma" -> "Windows Media Audio (WMA)" + + // Don't know, fall back to an extension + else -> mimeTypes.getExtensionFromMimeType(mime)?.uppercase() + } + } + companion object { fun from(song: Song): SongDetailDialog { val instance = SongDetailDialog() 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 62b0766d3..3038f2263 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -64,7 +64,7 @@ data class Song( /** The URI linking to this song's file. */ val uri: Uri, /** The mime type of this song. */ - val mimeType: String, + val mimeType: MimeType, /** The size of this song (in bytes) */ val size: Long, /** The total duration of this song, in millis. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/PathFramework.kt b/app/src/main/java/org/oxycblt/auxio/music/PathFramework.kt index df8a57aee..f677e39af 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/PathFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/PathFramework.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.music import android.content.Context +import android.webkit.MimeTypeMap import org.oxycblt.auxio.R /** @@ -65,3 +66,57 @@ sealed class Dir { } } } + +/** + * Represents a mime type as it is loaded by Auxio. [fromExtension] is based on the file extension + * should always exist, while [fromFormat] is based on the file itself and may not be available. + */ +data class MimeType(val fromExtension: String, val fromFormat: String?) { + fun resolveName(context: Context): String { + // Prefer the format mime type first, as it actually is derived from the file + // and not the extension. Just make sure to ignore audio/raw, as that could feasibly + // correspond to multiple formats. + val readableMime = + if (fromFormat != null && fromFormat != "audio/raw") { + fromFormat + } else { + fromExtension + } + + // We have special names for the most common formats. + val readableStringRes = + when (readableMime) { + // Classic formats + "audio/mpeg", + "audio/mp3" -> R.string.cdc_mp3 + "audio/vorbis" -> R.string.cdc_ogg_vorbis + "audio/opus" -> R.string.cdc_ogg_opus + "audio/flac" -> R.string.cdc_flac + + // MP4, 3GPP, M4A, etc. are all based on AAC + "audio/mp4", + "audio/mp4a-latm", + "audio/mpeg4-generic", + "audio/aac", + "audio/3gpp", + "audio/3gpp2", -> R.string.cdc_aac + + // Windows formats + "audio/wav", + "audio/x-wav", + "audio/wave", + "audio/vnd.wave" -> R.string.cdc_wav + "audio/x-ms-wma" -> R.string.cdc_wma + + else -> -1 + } + + return if (readableStringRes > -1) { + context.getString(readableStringRes) + } else { + // Fall back to the extension if we can't find a special name for this format. + MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase() + ?: context.getString(R.string.def_codec) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt index 97fce0359..aee092549 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt @@ -140,15 +140,22 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { return null } - val metadata = + val format = try { - future.get()[0].getFormat(0).metadata + future.get()[0].getFormat(0) } catch (e: Exception) { logW("Unable to extract metadata for ${audio.title}") logW(e.stackTraceToString()) null } + if (format == null) { + logD("Nothing could be extracted for ${audio.title}") + return audio.toSong() + } + + format.sampleMimeType?.let { audio.formatMimeType = it } + val metadata = format.metadata if (metadata != null) { completeAudio(metadata) } else { diff --git a/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt index b14867384..cb9c58f24 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt @@ -28,6 +28,7 @@ import androidx.core.database.getStringOrNull import java.io.File import org.oxycblt.auxio.music.Dir import org.oxycblt.auxio.music.Indexer +import org.oxycblt.auxio.music.MimeType import org.oxycblt.auxio.music.Path import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.albumCoverUri @@ -216,7 +217,7 @@ abstract class MediaStoreBackend : Indexer.Backend { audio.id = cursor.getLong(idIndex) audio.title = cursor.getString(titleIndex) - audio.mimeType = cursor.getString(mimeTypeIndex) + audio.extensionMimeType = cursor.getString(mimeTypeIndex) audio.size = cursor.getLong(sizeIndex) // Try to use the DISPLAY_NAME field to obtain a (probably sane) file name @@ -266,7 +267,8 @@ abstract class MediaStoreBackend : Indexer.Backend { var title: String? = null, var displayName: String? = null, var dir: Dir? = null, - var mimeType: String? = null, + var extensionMimeType: String? = null, + var formatMimeType: String? = null, var size: Long? = null, var duration: Long? = null, var track: Int? = null, @@ -288,7 +290,11 @@ abstract class MediaStoreBackend : Indexer.Backend { name = requireNotNull(displayName) { "Malformed audio: No display name" }, parent = requireNotNull(dir) { "Malformed audio: No parent directory" }), uri = requireNotNull(id) { "Malformed audio: No id" }.audioUri, - mimeType = requireNotNull(mimeType) { "Malformed audio: No mime type" }, + mimeType = + MimeType( + fromExtension = + requireNotNull(extensionMimeType) { "Malformed audio: No mime type" }, + fromFormat = formatMimeType), size = requireNotNull(size) { "Malformed audio: No size" }, durationMs = requireNotNull(duration) { "Malformed audio: No duration" }, track = track, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27f127a6a..4d85879a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,7 +49,7 @@ Go to album View properties - File properties + Song properties File name Relative path Format @@ -169,12 +169,22 @@ No Date No Track Number No music playing - Unknown Format + Unknown Format No Bitrate No Sample Rate Song Name Artist Name + + MPEG-1 Layer 3 (MP3) + OGG + OGG Vorbis + OGG Opus + Free Lossless Audio Codec (FLAC) + Advanced Audio Coding (AAC) + Microsoft WAV + Windows Media Audio (WMA) + Red Pink