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_disc, zipped))
} }
add(SongProperty(R.string.lbl_path, song.path.resolve(context))) add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
info.resolvedMimeType.resolveName(context)?.let { // info.format.resolveName(context)?.let {
add(SongProperty(R.string.lbl_format, it)) // add(SongProperty(R.string.lbl_format, it))
} // }
add( add(
SongProperty( SongProperty(
R.string.lbl_size, Formatter.formatFileSize(context, song.size))) 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>() val mimeTypes = mutableSetOf<String>()
for (song in songs) { for (song in songs) {
builder.addStream(song.uri) builder.addStream(song.uri)
mimeTypes.add(song.mimeType.fromFormat ?: song.mimeType.fromExtension) mimeTypes.add(song.format.mimeType)
} }
builder.setType(mimeTypes.singleOrNull() ?: "audio/*").startChooser() 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.concatLocalized
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
import org.oxycblt.musikr.cover.Cover 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.fs.Path
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.Disc import org.oxycblt.musikr.tag.Disc
@ -252,8 +252,8 @@ interface Song : Music {
* instead for accessing the audio file. * instead for accessing the audio file.
*/ */
val path: Path val path: Path
/** The [MimeType] of the audio file. Only intended for display. */ /** The [Format] of the audio file. Only intended for display. */
val mimeType: MimeType val format: Format
/** The size of the audio file, in bytes. */ /** The size of the audio file, in bytes. */
val size: Long val size: Long
/** The duration of the audio file, in milliseconds. */ /** The duration of the audio file, in milliseconds. */

View file

@ -18,6 +18,7 @@
package org.oxycblt.musikr.cache package org.oxycblt.musikr.cache
import org.oxycblt.ktaglib.Properties
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.tag.parse.ParsedTags 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 { private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
override suspend fun read(file: DeviceFile) = override suspend fun read(file: DeviceFile) =

View file

@ -30,6 +30,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters import androidx.room.TypeConverters
import org.oxycblt.ktaglib.Properties
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date
@ -67,7 +68,6 @@ internal data class CachedInfo(
*/ */
@PrimaryKey val uri: String, @PrimaryKey val uri: String,
val dateModified: Long, val dateModified: Long,
val durationMs: Long,
val replayGainTrackAdjustment: Float?, val replayGainTrackAdjustment: Float?,
val replayGainAlbumAdjustment: Float?, val replayGainAlbumAdjustment: Float?,
val musicBrainzId: String?, val musicBrainzId: String?,
@ -88,7 +88,11 @@ internal data class CachedInfo(
val albumArtistNames: List<String>, val albumArtistNames: List<String>,
val albumArtistSortNames: List<String>, val albumArtistSortNames: List<String>,
val genreNames: 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() = fun intoCachedSong() =
CachedSong( CachedSong(
@ -114,7 +118,8 @@ internal data class CachedInfo(
albumArtistNames = albumArtistNames, albumArtistNames = albumArtistNames,
albumArtistSortNames = albumArtistSortNames, albumArtistSortNames = albumArtistSortNames,
genreNames = genreNames), genreNames = genreNames),
cover) cover,
Properties(mimeType, durationMs, bitrate, sampleRate))
object Converters { object Converters {
@TypeConverter @TypeConverter
@ -159,6 +164,9 @@ internal data class CachedInfo(
albumArtistNames = cachedSong.parsedTags.albumArtistNames, albumArtistNames = cachedSong.parsedTags.albumArtistNames,
albumArtistSortNames = cachedSong.parsedTags.albumArtistSortNames, albumArtistSortNames = cachedSong.parsedTags.albumArtistSortNames,
genreNames = cachedSong.parsedTags.genreNames, 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 dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.fs.MimeType
import timber.log.Timber as L 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 bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
* @param sampleRateHz The sample rate, in hertz. * @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) * @author Alexander Capehart (OxygenCobalt)
*/ */
data class AudioProperties( data class AudioProperties(val bitrateKbps: Int?, val sampleRateHz: Int?) {
val bitrateKbps: Int?,
val sampleRateHz: Int?,
val resolvedMimeType: MimeType
) {
/** Implements the process of extracting [AudioProperties] from a given [Song]. */ /** Implements the process of extracting [AudioProperties] from a given [Song]. */
interface Factory { interface Factory {
/** /**
@ -75,7 +69,7 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
// that we can show. // that we can show.
L.w("Unable to extract song attributes.") L.w("Unable to extract song attributes.")
L.w(e.stackTraceToString()) 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 // 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 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() extractor.release()
L.d("Finished extracting audio properties") L.d("Finished extracting audio properties")
return AudioProperties( return AudioProperties(bitrate, sampleRate)
bitrate,
sampleRate,
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType))
} }
} }

View file

@ -49,7 +49,7 @@ class SongImpl(private val handle: SongCore) : Song {
override val date = preSong.date override val date = preSong.date
override val uri = preSong.uri override val uri = preSong.uri
override val path = preSong.path override val path = preSong.path
override val mimeType = preSong.mimeType override val format = preSong.format
override val size = preSong.size override val size = preSong.size
override val durationMs = preSong.durationMs override val durationMs = preSong.durationMs
override val replayGainAdjustment = preSong.replayGainAdjustment override val replayGainAdjustment = preSong.replayGainAdjustment

View file

@ -53,7 +53,10 @@ private class EvaluateStepImpl(
val preSongs = val preSongs =
extractedMusic extractedMusic
.filterIsInstance<ExtractedMusic.Song>() .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) .flowOn(Dispatchers.Main)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val graphBuilder = MusicGraph.builder() val graphBuilder = MusicGraph.builder()

View file

@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import org.oxycblt.ktaglib.Properties
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.CachedSong import org.oxycblt.musikr.cache.CachedSong
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
@ -63,7 +64,7 @@ private class ExtractStepImpl(
val (cachedSongs, uncachedSongs) = val (cachedSongs, uncachedSongs) =
cacheResults.mapPartition { cacheResults.mapPartition {
it.cachedSong?.let { song -> 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) val split = uncachedSongs.distribute(16)
@ -73,10 +74,9 @@ private class ExtractStepImpl(
.mapNotNull { node -> .mapNotNull { node ->
val metadata = val metadata =
metadataExtractor.extract(node.file) ?: return@mapNotNull null metadataExtractor.extract(node.file) ?: return@mapNotNull null
L.d("Extracted tags for ${metadata.id3v2}")
val tags = tagParser.parse(node.file, metadata) val tags = tagParser.parse(node.file, metadata)
val cover = metadata.cover?.let { storage.storedCovers.write(it) } 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) .flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
@ -84,7 +84,7 @@ private class ExtractStepImpl(
val writtenSongs = val writtenSongs =
merge(*extractedSongs) merge(*extractedSongs)
.map { .map {
storage.cache.write(it.file, CachedSong(it.tags, it.cover)) storage.cache.write(it.file, CachedSong(it.tags, it.cover, it.properties))
it it
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
@ -100,6 +100,10 @@ private class ExtractStepImpl(
} }
sealed interface ExtractedMusic { sealed interface ExtractedMusic {
data class Song(val file: DeviceFile, val tags: ParsedTags, val cover: Cover.Single?) : data class Song(
ExtractedMusic 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.auxio.util.update
import org.oxycblt.musikr.Music import org.oxycblt.musikr.Music
import org.oxycblt.musikr.cover.Cover 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.fs.Path
import org.oxycblt.musikr.playlist.PlaylistHandle import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date
@ -42,7 +42,7 @@ data class PreSong(
val date: Date?, val date: Date?,
val uri: Uri, val uri: Uri,
val path: Path, val path: Path,
val mimeType: MimeType, val format: Format,
val size: Long, val size: Long,
val durationMs: Long, val durationMs: Long,
val replayGainAdjustment: ReplayGainAdjustment, val replayGainAdjustment: ReplayGainAdjustment,

View file

@ -21,9 +21,10 @@ package org.oxycblt.musikr.tag.interpret
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
import org.oxycblt.ktaglib.Properties
import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.cover.Cover 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.fs.query.DeviceFile
import org.oxycblt.musikr.tag.Disc import org.oxycblt.musikr.tag.Disc
import org.oxycblt.musikr.tag.Name import org.oxycblt.musikr.tag.Name
@ -36,6 +37,7 @@ interface TagInterpreter {
file: DeviceFile, file: DeviceFile,
parsedTags: ParsedTags, parsedTags: ParsedTags,
cover: Cover.Single?, cover: Cover.Single?,
properties: Properties,
interpretation: Interpretation interpretation: Interpretation
): PreSong ): PreSong
@ -49,6 +51,7 @@ private data object TagInterpreterImpl : TagInterpreter {
file: DeviceFile, file: DeviceFile,
parsedTags: ParsedTags, parsedTags: ParsedTags,
cover: Cover.Single?, cover: Cover.Single?,
properties: Properties,
interpretation: Interpretation interpretation: Interpretation
): PreSong { ): PreSong {
val individualPreArtists = val individualPreArtists =
@ -79,7 +82,6 @@ private data object TagInterpreterImpl : TagInterpreter {
date = parsedTags.date, date = parsedTags.date,
uri = uri, uri = uri,
path = file.path, path = file.path,
mimeType = MimeType(file.mimeType, null),
size = file.size, size = file.size,
durationMs = parsedTags.durationMs, durationMs = parsedTags.durationMs,
replayGainAdjustment = replayGainAdjustment =
@ -87,6 +89,7 @@ private data object TagInterpreterImpl : TagInterpreter {
parsedTags.replayGainTrackAdjustment, parsedTags.replayGainTrackAdjustment,
parsedTags.replayGainAlbumAdjustment, parsedTags.replayGainAlbumAdjustment,
), ),
format = Format.infer(file.mimeType, properties.mimeType),
lastModified = file.lastModified, lastModified = file.lastModified,
// TODO: Figure out what to do with date added // TODO: Figure out what to do with date added
dateAdded = file.lastModified, dateAdded = file.lastModified,