Skip to content

Commit

Permalink
refactor lyric holder away and compute end time in backend
Browse files Browse the repository at this point in the history
  • Loading branch information
nift4 committed Jan 25, 2025
1 parent 1b7f9a4 commit 542e5f6
Show file tree
Hide file tree
Showing 9 changed files with 796 additions and 778 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ fun MediaController.setTimer(value: Int) {
)
}

inline fun <reified T> MutableList<T>.forEachSupport(skipFirst: Int = 0, operator: (T) -> Unit) {
val li = listIterator()
var skip = skipFirst
while (skip-- > 0) {
li.next()
}
while (li.hasNext()) {
operator(li.next())
}
}

inline fun <reified T> MutableList<T>.replaceAllSupport(skipFirst: Int = 0, operator: (T) -> T) {
val li = listIterator()
var skip = skipFirst
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,9 +800,9 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
val cPos = (controller?.contentPosition ?: 0).toULong()
val nextUpdate = if (syncedLyrics != null) {
syncedLyrics?.text?.flatMap {
if (hnw && it.lyric.start <= cPos) listOf() else if (hnw) listOf(it.lyric.start) else
(it.lyric.words?.map { it.timeRange.start }?.filter { it > cPos } ?: listOf())
.let { i -> if (it.lyric.start > cPos) i + it.lyric.start else i }
if (hnw && it.start <= cPos) listOf() else if (hnw) listOf(it.start) else
(it.words?.map { it.timeRange.start }?.filter { it > cPos } ?: listOf())
.let { i -> if (it.start > cPos) i + it.start else i }
}?.minOrNull()
} else if (lyricsLegacy != null) {
lyricsLegacy?.find {
Expand All @@ -820,7 +820,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
val isStatusBarLyricsEnabled = prefs.getBooleanStrict("status_bar_lyrics", false)
val highlightedLyric = if (isStatusBarLyricsEnabled && controller?.playWhenReady == true)
getCurrentLyricIndex(false)?.let {
syncedLyrics?.text?.get(it)?.lyric?.text ?: lyricsLegacy?.get(it)?.content
syncedLyrics?.text?.get(it)?.text ?: lyricsLegacy?.get(it)?.content
}
else null
if (lastSentHighlightedLyric != highlightedLyric) {
Expand All @@ -832,7 +832,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
fun getCurrentLyricIndex(withTranslation: Boolean) =
if (syncedLyrics != null) {
syncedLyrics?.text?.indexOfLast {
it.lyric.start <= (controller?.currentPosition ?: 0).toULong()
it.start <= (controller?.currentPosition ?: 0).toULong()
&& (!it.isTranslated || withTranslation)
}?.let { if (it == -1) null else it }
} else if (lyricsLegacy != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ class MyGradientSpan(val gradientWidth: Float, color: Int, highlightColor: Int)
val preOffsetFromLeft = lineOffsets[o].toFloat()
val textLength = lineOffsets[o + 1]
val isRtl = lineOffsets[o + 5] == -1
val ourProgress = lerpInv(lineOffsets[o + 2].toFloat(), lineOffsets[o + 3].toFloat(),
lerp(0f, totalCharsForProgress.toFloat(), progress)).coerceIn(0f, 1f)
val ourProgress = /*lerpInv(lineOffsets[o + 2].toFloat(), lineOffsets[o + 3].toFloat(),
lerp(0f, totalCharsForProgress.toFloat(), progress)).coerceIn(0f, 1f)*/1f
val ourProgressD = if (isRtl) 1f - ourProgress else ourProgress
shader.setLocalMatrix(matrix.apply {
// step 0: gradient is |>>-------| where > is the gradient and - is clamping color
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment
import java.io.File
import java.nio.charset.Charset
import kotlin.math.min
import org.akanework.gramophone.logic.forEachSupport
import org.akanework.gramophone.logic.replaceAllSupport
import org.akanework.gramophone.logic.utils.SemanticLyrics.Word

Expand All @@ -38,10 +39,9 @@ object LrcUtils {
} catch (e: Exception) {
Log.e(TAG, Log.getStackTraceString(e))
SemanticLyrics.UnsyncedLyrics(listOf(parserOptions.errorText))
})?.let {
})?.also {
if (it is SemanticLyrics.SyncedLyrics)
splitBidirectionalWords(it)
else it
}
}

Expand Down Expand Up @@ -92,32 +92,31 @@ object LrcUtils {
}
}

private fun splitBidirectionalWords(syncedLyrics: SemanticLyrics.SyncedLyrics): SemanticLyrics.SyncedLyrics {
return SemanticLyrics.SyncedLyrics(syncedLyrics.text.map { line ->
if (line.lyric.words.isNullOrEmpty()) return@map line
val bidirectionalBarriers = findBidirectionalBarriers(line.lyric.text)
val wordsWithBarriers = line.lyric.words.toMutableList()
private fun splitBidirectionalWords(syncedLyrics: SemanticLyrics.SyncedLyrics) {
syncedLyrics.text.forEach { line ->
if (line.words.isNullOrEmpty()) return@forEach
val bidirectionalBarriers = findBidirectionalBarriers(line.text)
var lastWasRtl = false
bidirectionalBarriers.forEach { barrier ->
val evilWordIndex =
if (barrier.first == -1) -1 else wordsWithBarriers.indexOfFirst {
if (barrier.first == -1) -1 else line.words.indexOfFirst {
it.charRange.contains(barrier.first) && it.charRange.start != barrier.first
}
if (evilWordIndex == -1) {
// Propagate the new direction (if there is a barrier after that, direction will
// be corrected after it).
val wordIndex = if (barrier.first == -1) 0 else
wordsWithBarriers.indexOfFirst { it.charRange.start == barrier.first }
wordsWithBarriers.replaceAllSupport(skipFirst = wordIndex) {
if (it.isRtl != barrier.second) it.copy(isRtl = barrier.second) else it
line.words.indexOfFirst { it.charRange.start == barrier.first }
line.words.forEachSupport(skipFirst = wordIndex) {
it.isRtl = barrier.second
}
lastWasRtl = barrier.second
return@forEach
}
val evilWord = wordsWithBarriers[evilWordIndex]
val evilWord = line.words[evilWordIndex]
// Estimate how long this word will take based on character to time ratio. To avoid
// this estimation, add a word sync point to bidirectional barriers :)
val barrierTime = min(evilWord.timeRange.first + ((line.lyric.words.map {
val barrierTime = min(evilWord.timeRange.first + ((line.words.map {
it.timeRange.count() / it.charRange.count().toFloat()
}.average().let { if (it.isNaN()) 100.0 else it } * (barrier.first -
evilWord.charRange.first))).toULong(), evilWord.timeRange.last - 1uL)
Expand All @@ -129,12 +128,11 @@ object LrcUtils {
charRange = barrier.first..evilWord.charRange.last,
timeRange = barrierTime..evilWord.timeRange.last, isRtl = barrier.second
)
wordsWithBarriers[evilWordIndex] = firstPart
wordsWithBarriers.add(evilWordIndex + 1, secondPart)
line.words[evilWordIndex] = firstPart
line.words.add(evilWordIndex + 1, secondPart)
lastWasRtl = barrier.second
}
line.copy(line.lyric.copy(words = wordsWithBarriers))
})
}
}

private val ltr =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
import org.akanework.gramophone.logic.utils.SemanticLyrics.LyricLine
import org.akanework.gramophone.logic.utils.SemanticLyrics.LyricLineHolder
import org.akanework.gramophone.logic.utils.SemanticLyrics.SyncedLyrics
import org.akanework.gramophone.logic.utils.SemanticLyrics.UnsyncedLyrics
import org.akanework.gramophone.logic.utils.SemanticLyrics.Word
Expand Down Expand Up @@ -365,30 +364,26 @@ sealed class SemanticLyrics : Parcelable {
data class UnsyncedLyrics(override val unsyncedText: List<String>) : SemanticLyrics()

@Parcelize
data class SyncedLyrics(val text: List<LyricLineHolder>) : SemanticLyrics() {
data class SyncedLyrics(val text: List<LyricLine>) : SemanticLyrics() {
override val unsyncedText: List<String>
get() = text.map { it.lyric.text }
get() = text.map { it.text }
}

@Parcelize
data class LyricLine(
val text: String,
val start: ULong,
val words: List<Word>?,
val speaker: SpeakerEntity?
) : Parcelable

@Parcelize
data class LyricLineHolder(
val lyric: LyricLine,
val isTranslated: Boolean
var end: ULong,
val words: MutableList<Word>?,
val speaker: SpeakerEntity?,
var isTranslated: Boolean
) : Parcelable

@Parcelize
data class Word(
val timeRange: @WriteWith<ULongRangeParceler>() ULongRange,
val charRange: @WriteWith<IntRangeParceler>() IntRange,
val isRtl: Boolean
var timeRange: @WriteWith<ULongRangeParceler>() ULongRange,
var charRange: @WriteWith<IntRangeParceler>() IntRange,
var isRtl: Boolean
) : Parcelable

object ULongRangeParceler : Parceler<ULongRange> {
Expand Down Expand Up @@ -488,9 +483,11 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
// next char is even rendered (or whitespace).
// TODO is this working?
val textWithoutStartWhitespace = current.second!!.trimStart()
val startWhitespaceLength = current.second!!.length - textWithoutStartWhitespace.length
val startWhitespaceLength =
current.second!!.length - textWithoutStartWhitespace.length
val textWithoutWhitespaces = textWithoutStartWhitespace.trimEnd()
val endWhitespaceLength = textWithoutStartWhitespace.length - textWithoutWhitespaces.length
val endWhitespaceLength =
textWithoutStartWhitespace.length - textWithoutWhitespaces.length
val startIndex = oIdx + startWhitespaceLength
val endIndex = idx - endWhitespaceLength
if (startIndex == endIndex)
Expand All @@ -509,14 +506,23 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
// Estimate how long this word will take based on character
// to time ratio. To avoid this estimation, add a last word
// sync point to the line after the text :)
current.first + (wout.map { it.timeRange.count() /
it.charRange.count().toFloat() }.average().let {
if (it.isNaN()) 100.0 else it } *
current.first + (wout.map {
it.timeRange.count() /
it.charRange.count().toFloat()
}.average().let {
if (it.isNaN()) 100.0 else it
} *
textWithoutWhitespaces.length).toULong()
}
if (endInclusive > current.first)
// isRtl is filled in later in splitBidirectionalWords
wout.add(Word(current.first..endInclusive, startIndex..<endIndex, isRtl = false))
wout.add(
Word(
current.first..endInclusive,
startIndex..<endIndex,
isRtl = false
)
)
}
wout
} else null
Expand Down Expand Up @@ -544,14 +550,14 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
}
val start = if (currentLine.isNotEmpty()) currentLine.first().first
else lastWordSyncPoint ?: lastSyncPoint!!
out.add(LyricLine(text, start, words, speaker))
out.add(LyricLine(text, start, 0uL /* filled later */, words, speaker, false /* filled later */))
compressed.forEach {
val diff = it - start
out.add(out.last().copy(start = it, words = words?.map {
it.copy(
it.timeRange.start + diff..it.timeRange.last + diff
)
}))
}?.toMutableList()))
}
}
compressed.clear()
Expand All @@ -572,7 +578,7 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
var previousTimestamp = 0uL
val defaultIsWalaokeM = out.find { it.speaker?.isWalaoke == true } != null &&
out.find { it.speaker?.isWalaoke == false } == null
return SyncedLyrics(out.flatMap {
val out2 = out.flatMap {
if (sawNonBlank || it.text.isNotBlank()) {
sawNonBlank = true
listOf(it)
Expand All @@ -581,11 +587,17 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
if (defaultIsWalaokeM && it.speaker == null)
it.copy(speaker = SpeakerEntity.Male)
else it
}.map {
LyricLineHolder(it, it.start == previousTimestamp).also {
previousTimestamp = it.lyric.start
}
})
}
out2.forEachIndexed { i, lyric ->
lyric.end = lyric.words?.lastOrNull()?.timeRange?.last
?: (if (lyric.start == previousTimestamp) out2.find { it.start == lyric.start }
?.words?.lastOrNull()?.timeRange?.last else null)
?: out2.find { it.start > lyric.start }?.start?.minus(1uL)
?: Long.MAX_VALUE.toULong()
lyric.isTranslated = lyric.start == previousTimestamp
previousTimestamp = lyric.start
}
return SyncedLyrics(out2)
}

fun parseTtml(lyricText: String, trimEnabled: Boolean): SemanticLyrics? {
Expand All @@ -602,7 +614,7 @@ fun SemanticLyrics?.convertForLegacy(): MutableList<MediaStoreUtils.Lyric>? {
if (this == null) return null
if (this is SyncedLyrics) {
return this.text.map {
MediaStoreUtils.Lyric(it.lyric.start.toLong(), it.lyric.text, it.isTranslated)
MediaStoreUtils.Lyric(it.start.toLong(), it.text, it.isTranslated)
}.toMutableList()
}
return mutableListOf(MediaStoreUtils.Lyric(null, this.unsyncedText.joinToString("\n"), false))
Expand Down
17 changes: 8 additions & 9 deletions app/src/main/kotlin/org/akanework/gramophone/ui/Widget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,10 @@ private class LyricRemoteViewsFactory(private val context: Context, private val
val itemLegacy = service?.lyricsLegacy?.getOrNull(position)
if (item == null && itemUnsynced == null && itemLegacy == null) return null
val isTranslation = (item?.isTranslated ?: itemLegacy?.isTranslation) == true
val isBackground = item?.lyric?.speaker?.isBackground == true
val isVoice2 = item?.lyric?.speaker?.isVoice2 == true
val startTs = item?.lyric?.start?.toLong() ?: itemLegacy?.timeStamp ?: -1L
val endTs = item?.lyric?.words?.lastOrNull()?.timeRange?.last?.toLong()
?: service?.syncedLyrics?.text?.find { it.lyric.start > item!!.lyric.start }?.lyric?.start?.toLong()?.minus(1)
val isBackground = item?.speaker?.isBackground == true
val isVoice2 = item?.speaker?.isVoice2 == true
val startTs = item?.start?.toLong() ?: itemLegacy?.timeStamp ?: -1L
val endTs = item?.end?.toLong()
?: service?.lyricsLegacy?.find { (it.timeStamp ?: -1L) > (itemLegacy!!.timeStamp ?: -1L) }?.timeStamp?.minus(1)
?: Long.MAX_VALUE
val isActive = startTs == -1L || cPos != null && cPos >= startTs && cPos <= endTs
Expand All @@ -177,9 +176,9 @@ private class LyricRemoteViewsFactory(private val context: Context, private val
else -> R.layout.lyric_widget_text_left
}
).apply {
val sb = SpannableString(item?.lyric?.text ?: itemUnsynced ?: itemLegacy!!.content)
val sb = SpannableString(item?.text ?: itemUnsynced ?: itemLegacy!!.content)
if (isActive) {
val hlChar = item?.lyric?.words?.findLast { it.timeRange.start <= cPos!!.toULong() }
val hlChar = item?.words?.findLast { it.timeRange.start <= cPos!!.toULong() }
?.charRange?.last?.plus(1) ?: sb.length
sb.setSpan(span, 0, hlChar, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
Expand All @@ -206,9 +205,9 @@ private class LyricRemoteViewsFactory(private val context: Context, private val
val itemUnsynced = service?.lyrics?.unsyncedText?.getOrNull(position)
val itemLegacy = service?.lyricsLegacy?.getOrNull(position)
if (item == null && itemUnsynced == null && itemLegacy == null) return 0L
return ((item?.lyric?.text ?: itemUnsynced ?: itemLegacy!!.content).hashCode()).toLong()
return ((item?.text ?: itemUnsynced ?: itemLegacy!!.content).hashCode()).toLong()
.shl(32) or
(item?.lyric?.start?.hashCode() ?: itemLegacy?.timeStamp?.hashCode()
(item?.start?.hashCode() ?: itemLegacy?.timeStamp?.hashCode()
?: -1L).toLong()
}

Expand Down
Loading

0 comments on commit 542e5f6

Please sign in to comment.