Skip to content

Commit

Permalink
rtl detection for lyrics
Browse files Browse the repository at this point in the history
  • Loading branch information
nift4 committed Oct 28, 2024
1 parent 03c6f7b commit 585c32d
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 275 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.extractor.metadata.id3.BinaryFrame
import androidx.media3.extractor.metadata.id3.TextInformationFrame
import androidx.media3.extractor.metadata.vorbis.VorbisComment
import org.akanework.gramophone.logic.utils.SemanticLyrics.Word
import java.io.File
import java.nio.charset.Charset

Expand All @@ -20,7 +21,7 @@ object LrcUtils {

@VisibleForTesting
fun parseLyrics(lyrics: String, parserOptions: LrcParserOptions): SemanticLyrics? {
return try {
return (try {
parseTtml(lyrics, parserOptions.trim)
} catch (e: Exception) {
Log.e(TAG, Log.getStackTraceString(e))
Expand All @@ -35,7 +36,11 @@ object LrcUtils {
} catch (e: Exception) {
Log.e(TAG, Log.getStackTraceString(e))
SemanticLyrics.UnsyncedLyrics(listOf(parserOptions.errorText))
}
})?.let {
if (it is SemanticLyrics.SyncedLyrics)
splitBidirectionalWords(it)
else it
}
}

@OptIn(UnstableApi::class)
Expand Down Expand Up @@ -77,6 +82,63 @@ object LrcUtils {
}
}

private fun splitBidirectionalWords(syncedLyrics: SemanticLyrics.SyncedLyrics): SemanticLyrics.SyncedLyrics {
return SemanticLyrics.SyncedLyrics(syncedLyrics.text.map { line ->
if (line.lyric.words == null) return@map line
val bidirectionalBarriers = findBidirectionalBarriers(line.lyric.text)
val wordsWithBarriers = line.lyric.words.toMutableList()
var lastWasRtl = false
bidirectionalBarriers.forEach { barrier ->
val evilWordIndex = if (barrier.first == -1) -1 else wordsWithBarriers.indexOfFirst {
it.charRange.contains(barrier.first) && it.charRange.start != barrier.first }
if (evilWordIndex == -1) {
val wordIndex = if (barrier.first == -1) 0 else
wordsWithBarriers.indexOfFirst { it.charRange.start == barrier.first }
if (wordsWithBarriers[wordIndex].isRtl != barrier.second)
wordsWithBarriers[wordIndex] = wordsWithBarriers[wordIndex].copy(isRtl = barrier.second)
lastWasRtl = barrier.second
return@forEach
}
val evilWord = wordsWithBarriers[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 = evilWord.timeRange.first + ((line.lyric.words.map {
it.timeRange.count() / it.charRange.count().toFloat()
}.average() * (barrier.first - evilWord.charRange.first))).toULong()
val firstPart = Word(charRange = evilWord.charRange.first..<barrier.first,
timeRange = evilWord.timeRange.first..<barrierTime, isRtl = lastWasRtl)
val secondPart = Word(charRange = barrier.first..evilWord.charRange.last,
timeRange = barrierTime..evilWord.timeRange.last, isRtl = barrier.second)
wordsWithBarriers[evilWordIndex] = firstPart
wordsWithBarriers.add(evilWordIndex + 1, secondPart)
lastWasRtl = barrier.second
}
line.copy(line.lyric.copy(words = wordsWithBarriers))
})
}

private val ltr = arrayOf(Character.DIRECTIONALITY_LEFT_TO_RIGHT, Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING, Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE)
private val rtl = arrayOf(Character.DIRECTIONALITY_RIGHT_TO_LEFT, Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC, Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING, Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE)
private fun findBidirectionalBarriers(text: String): List<Pair<Int, Boolean>> {
val barriers = mutableListOf<Pair<Int, Boolean>>()
if (text.isEmpty()) return barriers
var previousDirection = text.find {
val dir = Character.getDirectionality(it)
dir in ltr || dir in rtl
}?.let { Character.getDirectionality(it) in rtl } == true
barriers.add(Pair(-1, previousDirection))
for (i in 0 until text.length) {
val currentDirection = Character.getDirectionality(text[i])
val isRtl = currentDirection in rtl
if (currentDirection !in ltr && !isRtl)
continue
if (previousDirection != isRtl)
barriers.add(Pair(i, isRtl))
previousDirection = isRtl
}
return barriers
}

@OptIn(UnstableApi::class)
fun extractAndParseLyricsLegacy(metadata: Metadata, parserOptions: LrcParserOptions): MutableList<MediaStoreUtils.Lyric>? {
for (i in 0..<metadata.length()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ sealed class SemanticLyrics : Parcelable {
data class LyricLineHolder(val lyric: LyricLine,
val isTranslated: Boolean) : Parcelable
@Parcelize
data class Word(val timeRange: @WriteWith<ULongRangeParceler>() ULongRange, val charRange: @WriteWith<IntRangeParceler>() IntRange) : Parcelable
data class Word(val timeRange: @WriteWith<ULongRangeParceler>() ULongRange, val charRange: @WriteWith<IntRangeParceler>() IntRange, val isRtl: Boolean) : Parcelable
object ULongRangeParceler : Parceler<ULongRange> {
override fun create(parcel: Parcel) = parcel.readLong().toULong()..parcel.readLong().toULong()

Expand Down Expand Up @@ -443,11 +443,13 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
// 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() *
it.charRange.count().toFloat() }.average()
.let { if (it.isNaN()) 100.0 else it } *
(current.second?.length ?: 0)).toULong()
}
if (endInclusive > current.first)
wout.add(Word(current.first..endInclusive, oIdx..<idx))
// isRtl is filled in later in splitBidirectionalWords
wout.add(Word(current.first..endInclusive, oIdx..<idx, isRtl = false))
}
wout
} else null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,19 +472,19 @@ class NewLyricsView(context: Context, attrs: AttributeSet) : View(context, attrs
var baseOffset = if (line == firstLine && !alignmentOpposite) {
val il = StaticLayoutBuilderCompat.obtain(sb, layout.paint, Int.MAX_VALUE)
.setStart(layout.getLineStart(line)).setEnd(it.charRange.first)
.setIsRtl(false).build()
.build()
il.getLineWidth(0).toInt()
} else if (alignmentOpposite) {
val il = StaticLayoutBuilderCompat.obtain(sb, layout.paint,
Int.MAX_VALUE).setStart(max(it.charRange.first,
layout.getLineStart(line))).setEnd(layout.getLineEnd(line))
.setIsRtl(false).build()
.build()
width - il.getLineWidth(0).toInt()
} else 0
ia.add(baseOffset + if (layout.alignment == Layout.Alignment.ALIGN_CENTER) {
val il = StaticLayoutBuilderCompat.obtain(sb, layout.paint, Int.MAX_VALUE)
.setStart(layout.getLineStart(line)).setEnd(layout.getLineEnd(line))
.setIsRtl(false).build()
.build()
((width - il.getLineWidth(0)) / 2).toInt()
} else 0) // offset from left to start from text on line
ia.add(layout.getLineBottom(line) - layout.getLineTop(line)) // line height
Expand Down
Loading

0 comments on commit 585c32d

Please sign in to comment.