Skip to content

Commit

Permalink
Wire up new lyric UI frame
Browse files Browse the repository at this point in the history
  • Loading branch information
nift4 committed Oct 20, 2024
1 parent bf8d7af commit b680790
Show file tree
Hide file tree
Showing 9 changed files with 657 additions and 429 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import org.akanework.gramophone.BuildConfig
import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_GET_LYRICS
import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_GET_LYRICS_LEGACY
import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_GET_SESSION
import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QUERY_TIMER
import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_SET_TIMER
import org.akanework.gramophone.logic.utils.MediaStoreUtils
import org.akanework.gramophone.logic.utils.SemanticLyrics
import org.akanework.gramophone.logic.utils.SemanticLyrics.LyricLineHolder
import org.jetbrains.annotations.Contract
import java.io.File
import java.util.Locale
Expand Down Expand Up @@ -213,15 +216,24 @@ inline fun <reified T> MutableList<T>.replaceAllSupport(operator: (T) -> T) {
}

@Suppress("UNCHECKED_CAST")
fun MediaController.getLyrics(): MutableList<MediaStoreUtils.Lyric>? =
fun MediaController.getLyricsLegacy(): MutableList<MediaStoreUtils.Lyric>? =
sendCustomCommand(
SessionCommand(SERVICE_GET_LYRICS, Bundle.EMPTY),
SessionCommand(SERVICE_GET_LYRICS_LEGACY, Bundle.EMPTY),
Bundle.EMPTY
).get().extras.let {
(BundleCompat.getParcelableArray(it, "lyrics", MediaStoreUtils.Lyric::class.java)
as Array<MediaStoreUtils.Lyric>?)?.toMutableList()
}

@Suppress("UNCHECKED_CAST")
fun MediaController.getLyrics(): SemanticLyrics? =
sendCustomCommand(
SessionCommand(SERVICE_GET_LYRICS, Bundle.EMPTY),
Bundle.EMPTY
).get().extras.let {
BundleCompat.getParcelable<SemanticLyrics>(it, "lyrics", SemanticLyrics::class.java)
}

@OptIn(UnstableApi::class)
@Suppress("UNCHECKED_CAST")
fun MediaController.getSessionId(): Int? =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ import org.akanework.gramophone.logic.utils.CircularShuffleOrder
import org.akanework.gramophone.logic.utils.LastPlayedManager
import org.akanework.gramophone.logic.utils.LrcUtils.LrcParserOptions
import org.akanework.gramophone.logic.utils.LrcUtils.extractAndParseLyrics
import org.akanework.gramophone.logic.utils.LrcUtils.extractAndParseLyricsLegacy
import org.akanework.gramophone.logic.utils.LrcUtils.loadAndParseLyricsFile
import org.akanework.gramophone.logic.utils.LrcUtils.loadAndParseLyricsFileLegacy
import org.akanework.gramophone.logic.utils.MediaStoreUtils
import org.akanework.gramophone.logic.utils.SemanticLyrics
import org.akanework.gramophone.logic.utils.exoplayer.EndedWorkaroundPlayer
import org.akanework.gramophone.logic.utils.exoplayer.GramophoneMediaSourceFactory
import org.akanework.gramophone.logic.utils.exoplayer.GramophoneRenderFactory
Expand Down Expand Up @@ -113,14 +116,16 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
const val SERVICE_SET_TIMER = "set_timer"
const val SERVICE_QUERY_TIMER = "query_timer"
const val SERVICE_GET_LYRICS = "get_lyrics"
const val SERVICE_GET_LYRICS_LEGACY = "get_lyrics_legacy"
const val SERVICE_GET_SESSION = "get_session"
const val SERVICE_TIMER_CHANGED = "changed_timer"
}

private var lastSessionId = 0
private var mediaSession: MediaLibrarySession? = null
private var controller: MediaController? = null
private var lyrics: MutableList<MediaStoreUtils.Lyric>? = null
private var lyrics: SemanticLyrics? = null
private var lyricsLegacy: MutableList<MediaStoreUtils.Lyric>? = null
private var shuffleFactory:
((Int) -> ((CircularShuffleOrder) -> Unit) -> CircularShuffleOrder)? = null
private lateinit var customCommands: List<CommandButton>
Expand Down Expand Up @@ -411,6 +416,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
availableSessionCommands.add(SessionCommand(SERVICE_GET_SESSION, Bundle.EMPTY))
availableSessionCommands.add(SessionCommand(SERVICE_QUERY_TIMER, Bundle.EMPTY))
availableSessionCommands.add(SessionCommand(SERVICE_GET_LYRICS, Bundle.EMPTY))
availableSessionCommands.add(SessionCommand(SERVICE_GET_LYRICS_LEGACY, Bundle.EMPTY))
handler.post {
session.sendCustomCommand(
controller,
Expand Down Expand Up @@ -486,7 +492,13 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis

SERVICE_GET_LYRICS -> {
SessionResult(SessionResult.RESULT_SUCCESS).also {
it.extras.putParcelableArray("lyrics", lyrics?.toTypedArray())
it.extras.putParcelable("lyrics", lyrics)
}
}

SERVICE_GET_LYRICS_LEGACY -> {
SessionResult(SessionResult.RESULT_SUCCESS).also {
it.extras.putParcelableArray("lyrics", lyricsLegacy?.toTypedArray())
}
}

Expand Down Expand Up @@ -544,31 +556,56 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
val multiLine = prefs.getBoolean("lyric_multiline", false)
val newParser = prefs.getBoolean("lyric_parser", false)
val options = LrcParserOptions(trim = trim, multiLine = multiLine,
legacyParser = !newParser, errorText = getString(
androidx.media3.session.R.string.error_message_io))
var lrc = loadAndParseLyricsFile(mediaItem?.getFile(), options)
if (lrc == null) {
loop@ for (i in tracks.groups) {
for (j in 0 until i.length) {
if (!i.isTrackSelected(j)) continue
// note: wav files can have null metadata
val trackMetadata = i.getTrackFormat(j).metadata ?: continue
lrc = extractAndParseLyrics(trackMetadata, options) ?: continue
// add empty element at the beginning
lrc.add(0, MediaStoreUtils.Lyric())
break@loop
errorText = getString(androidx.media3.session.R.string.error_message_io))
if (newParser) {
var lrc = loadAndParseLyricsFile(mediaItem?.getFile(), options)
if (lrc == null) {
loop@ for (i in tracks.groups) {
for (j in 0 until i.length) {
if (!i.isTrackSelected(j)) continue
// note: wav files can have null metadata
val trackMetadata = i.getTrackFormat(j).metadata ?: continue
lrc = extractAndParseLyrics(trackMetadata, options) ?: continue
break@loop
}
}
}
}
CoroutineScope(Dispatchers.Main).launch {
mediaSession?.let {
lyrics = lrc
it.broadcastCustomCommand(
SessionCommand(SERVICE_GET_LYRICS, Bundle.EMPTY),
Bundle.EMPTY
)
CoroutineScope(Dispatchers.Main).launch {
mediaSession?.let {
lyrics = lrc
lyricsLegacy = null
it.broadcastCustomCommand(
SessionCommand(SERVICE_GET_LYRICS, Bundle.EMPTY),
Bundle.EMPTY
)
}
}.join()
} else {
var lrc = loadAndParseLyricsFileLegacy(mediaItem?.getFile(), options)
if (lrc == null) {
loop@ for (i in tracks.groups) {
for (j in 0 until i.length) {
if (!i.isTrackSelected(j)) continue
// note: wav files can have null metadata
val trackMetadata = i.getTrackFormat(j).metadata ?: continue
lrc = extractAndParseLyricsLegacy(trackMetadata, options) ?: continue
// add empty element at the beginning
lrc.add(0, MediaStoreUtils.Lyric())
break@loop
}
}
}
}.join()
CoroutineScope(Dispatchers.Main).launch {
mediaSession?.let {
lyrics = null
lyricsLegacy = lrc
it.broadcastCustomCommand(
SessionCommand(SERVICE_GET_LYRICS, Bundle.EMPTY),
Bundle.EMPTY
)
}
}.join()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ object LrcUtils {

private const val TAG = "LrcUtils"

data class LrcParserOptions(val trim: Boolean, val multiLine: Boolean, val legacyParser: Boolean, val errorText: String)
data class LrcParserOptions(val trim: Boolean, val multiLine: Boolean, val errorText: String)

@OptIn(UnstableApi::class)
fun extractAndParseLyrics(metadata: Metadata, parserOptions: LrcParserOptions): MutableList<MediaStoreUtils.Lyric>? {
fun extractAndParseLyrics(metadata: Metadata, parserOptions: LrcParserOptions): SemanticLyrics? {
for (i in 0..<metadata.length()) {
val meta = metadata.get(i)
val data =
Expand All @@ -32,7 +32,32 @@ object LrcUtils {
else null
val lyrics = data?.let {
try {
parseLrcString(it, parserOptions)
SemanticLyrics.parse(it, parserOptions.trim, parserOptions.multiLine)
} catch (e: Exception) {
Log.e(TAG, Log.getStackTraceString(e))
SemanticLyrics.UnsyncedLyrics(listOf(parserOptions.errorText))
}
}
return lyrics ?: continue
}
return null
}

@OptIn(UnstableApi::class)
fun extractAndParseLyricsLegacy(metadata: Metadata, parserOptions: LrcParserOptions): MutableList<MediaStoreUtils.Lyric>? {
for (i in 0..<metadata.length()) {
val meta = metadata.get(i)
val data =
if (meta is VorbisComment && meta.key == "LYRICS") // ogg / flac
meta.value
else if (meta is BinaryFrame && (meta.id == "USLT" || meta.id == "SYLT")) // mp3 / other id3 based
UsltFrameDecoder.decode(ParsableByteArray(meta.data))
else if (meta is TextInformationFrame && (meta.id == "USLT" || meta.id == "SYLT")) // m4a
meta.values.joinToString("\n")
else null
val lyrics = data?.let {
try {
parseLrcStringLegacy(it, parserOptions)
} catch (e: Exception) {
Log.e(TAG, Log.getStackTraceString(e))
mutableListOf(MediaStoreUtils.Lyric(content = parserOptions.errorText))
Expand All @@ -44,11 +69,24 @@ object LrcUtils {
}

@OptIn(UnstableApi::class)
fun loadAndParseLyricsFile(musicFile: File?, parserOptions: LrcParserOptions): MutableList<MediaStoreUtils.Lyric>? {
fun loadAndParseLyricsFile(musicFile: File?, parserOptions: LrcParserOptions): SemanticLyrics? {
val lrcFile = musicFile?.let { File(it.parentFile, it.nameWithoutExtension + ".lrc") }
return loadLrcFile(lrcFile)?.let {
try {
SemanticLyrics.parse(it, parserOptions.trim, parserOptions.multiLine)
} catch (e: Exception) {
Log.e(TAG, Log.getStackTraceString(e))
SemanticLyrics.UnsyncedLyrics(listOf(parserOptions.errorText))
}
}
}

@OptIn(UnstableApi::class)
fun loadAndParseLyricsFileLegacy(musicFile: File?, parserOptions: LrcParserOptions): MutableList<MediaStoreUtils.Lyric>? {
val lrcFile = musicFile?.let { File(it.parentFile, it.nameWithoutExtension + ".lrc") }
return loadLrcFile(lrcFile)?.let {
try {
parseLrcString(it, parserOptions)
parseLrcStringLegacy(it, parserOptions)
} catch (e: Exception) {
Log.e(TAG, Log.getStackTraceString(e))
null
Expand All @@ -67,12 +105,10 @@ object LrcUtils {
}

@VisibleForTesting
fun parseLrcString(
fun parseLrcStringLegacy(
lrcContent: String,
parserOptions: LrcParserOptions
): MutableList<MediaStoreUtils.Lyric>? {
if (!parserOptions.legacyParser)
return SemanticLyrics.parseForLegacy(lrcContent, parserOptions.trim, parserOptions.multiLine)
if (lrcContent.isBlank()) return null

val timeMarksRegex = "\\[(\\d{2}:\\d{2})([.:]\\d+)?]".toRegex()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.akanework.gramophone.logic.utils

import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.map

Expand Down Expand Up @@ -27,7 +32,8 @@ import kotlin.collections.map
* We completely ignore all ID3 tags from the header as MediaStore is our source of truth.
*/

enum class SpeakerEntity(val isWakaloke: Boolean) {
@Parcelize
enum class SpeakerEntity(val isWakaloke: Boolean) : Parcelable {
Male(true), // Wakaloke
Female(true), // Wakaloke
Duet(true), // Wakaloke
Expand Down Expand Up @@ -314,18 +320,33 @@ private sealed class SyntacticLyrics {
* - [offset:] tag in header (ref Wikipedia)
* We completely ignore all ID3 tags from the header as MediaStore is our source of truth.
*/
sealed class SemanticLyrics {
sealed class SemanticLyrics : Parcelable {
abstract val unsyncedText: List<String>
@Parcelize
class UnsyncedLyrics(override val unsyncedText: List<String>) : SemanticLyrics()
class SyncedLyrics(val text: List<Pair<LyricLine, Boolean>>) : SemanticLyrics() {
@Parcelize
class SyncedLyrics(val text: List<LyricLineHolder>) : SemanticLyrics() {
override val unsyncedText: List<String>
get() = text.map { it.first.text }
get() = text.map { it.lyric.text }
}
@Parcelize
data class LyricLine(val text: String,
val start: ULong,
val words: List<Word>?,
val speaker: SpeakerEntity?)
data class Word(val timeRange: ULongRange, val charRange: ULongRange)
val speaker: SpeakerEntity?) : Parcelable
@Parcelize
data class LyricLineHolder(val lyric: LyricLine,
val isTranslated: Boolean) : Parcelable
@Parcelize
data class Word(val timeRange: @WriteWith<ULongRangeParceler>() ULongRange, val charRange: @WriteWith<ULongRangeParceler>() ULongRange) : Parcelable
object ULongRangeParceler : Parceler<ULongRange> {
override fun create(parcel: Parcel) = parcel.readLong().toULong()..parcel.readLong().toULong()

override fun ULongRange.write(parcel: Parcel, flags: Int) {
parcel.writeLong(first.toLong())
parcel.writeLong(last.toLong())
}
}
companion object {
fun parse(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean): SemanticLyrics? {
val lyricSyntax = SyntacticLyrics.parse(lyricText, multiLineEnabled)
Expand Down Expand Up @@ -463,18 +484,17 @@ sealed class SemanticLyrics {
it.copy(speaker = SpeakerEntity.Male)
else it
}.map {
Pair(it, it.start == previousTimestamp).also {
previousTimestamp = it.first.start
LyricLineHolder(it, it.start == previousTimestamp).also {
previousTimestamp = it.lyric.start
}
})
}

fun parseForLegacy(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean):
MutableList<MediaStoreUtils.Lyric>? {
val lyric = parse(lyricText, trimEnabled, multiLineEnabled) ?: return null
fun convertForLegacy(lyric: SemanticLyrics?): MutableList<MediaStoreUtils.Lyric>? {
if (lyric == null) return null
if (lyric is SyncedLyrics) {
return lyric.text.map {
MediaStoreUtils.Lyric(it.first.start.toLong(), it.first.text, it.second)
MediaStoreUtils.Lyric(it.lyric.start.toLong(), it.lyric.text, it.isTranslated)
}.toMutableList()
}
return mutableListOf(MediaStoreUtils.Lyric(null, lyric.unsyncedText.joinToString("\n"), false))
Expand Down
Loading

0 comments on commit b680790

Please sign in to comment.