detail: heavily improve format handling

Heavily improve the way Auxio handles shows formats in the song
properties view.

This is composed of the following:
- Using ExoPlayer to find a format-specific mime type before having to
fall back to other solutions
- Keeping around the format and extension mime types so that each is
picked in the best circumstances
- Using MediaFormat to also retrieve a format-specific mime type in the
case that ExoPlayer parsing is disabled
- Adding special names for the most common formats
This commit is contained in:
OxygenCobalt 2022-06-12 11:47:49 -06:00
parent 8e56459f8b
commit 5f0518d983
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 149 additions and 15 deletions

View file

@ -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) {

View file

@ -62,9 +62,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
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<DialogSongDetailBinding>() {
}
}
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()

View file

@ -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. */

View file

@ -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)
}
}
}

View file

@ -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 {

View file

@ -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,

View file

@ -49,7 +49,7 @@
<string name="lbl_go_album">Go to album</string>
<string name="lbl_song_detail">View properties</string>
<string name="lbl_props">File properties</string>
<string name="lbl_props">Song properties</string>
<string name="lbl_file_name">File name</string>
<string name="lbl_relative_path">Relative path</string>
<string name="lbl_format">Format</string>
@ -169,12 +169,22 @@
<string name="def_date">No Date</string>
<string name="def_track">No Track Number</string>
<string name="def_playback">No music playing</string>
<string name="def_format">Unknown Format</string>
<string name="def_codec">Unknown Format</string>
<string name="def_bitrate">No Bitrate</string>
<string name="def_sample_rate">No Sample Rate</string>
<string name="def_widget_song">Song Name</string>
<string name="def_widget_artist">Artist Name</string>
<!-- Codec Namespace | Format names -->
<string name="cdc_mp3">MPEG-1 Layer 3 (MP3)</string>
<string name="cdc_ogg">OGG</string>
<string name="cdc_ogg_vorbis">OGG Vorbis</string>
<string name="cdc_ogg_opus">OGG Opus</string>
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
<string name="cdc_aac">Advanced Audio Coding (AAC)</string>
<string name="cdc_wav">Microsoft WAV</string>
<string name="cdc_wma">Windows Media Audio (WMA)</string>
<!-- Color Label namespace | Accent names -->
<string name="clr_red">Red</string>
<string name="clr_pink">Pink</string>