musikr: replace mimetype w/format

First property now derived from taglib.
This commit is contained in:
Alexander Capehart 2024-12-13 19:23:42 -07:00
parent e16b23f34e
commit 9ab4dc5595
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 162 additions and 148 deletions

View file

@ -104,9 +104,9 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment<DialogSongDetailBindi
add(SongProperty(R.string.lbl_disc, zipped))
}
add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
info.resolvedMimeType.resolveName(context)?.let {
add(SongProperty(R.string.lbl_format, it))
}
// info.format.resolveName(context)?.let {
// add(SongProperty(R.string.lbl_format, it))
// }
add(
SongProperty(
R.string.lbl_size, Formatter.formatFileSize(context, song.size)))

View file

@ -320,7 +320,7 @@ fun Context.share(songs: Collection<Song>) {
val mimeTypes = mutableSetOf<String>()
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()

View file

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

View file

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

View file

@ -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<String>,
val albumArtistSortNames: List<String>,
val genreNames: List<String>,
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)
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}
}

View file

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

View file

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

View file

@ -53,7 +53,10 @@ private class EvaluateStepImpl(
val preSongs =
extractedMusic
.filterIsInstance<ExtractedMusic.Song>()
.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()

View file

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

View file

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

View file

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