Skip to content

Commit

Permalink
support rtl and bidi lyric
Browse files Browse the repository at this point in the history
  • Loading branch information
nift4 committed Oct 29, 2024
1 parent ef09b71 commit 511c5d0
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,12 @@ fun MediaController.setTimer(value: Int) {
)
}

inline fun <reified T> MutableList<T>.replaceAllSupport(operator: (T) -> T) {
inline fun <reified T> MutableList<T>.replaceAllSupport(skipFirst: Int = 0, operator: (T) -> T) {
val li = listIterator()
var skip = skipFirst
while (skip-- > 0) {
li.next()
}
while (li.hasNext()) {
li.set(operator(li.next()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import kotlin.math.max
import kotlin.math.min

// Hacks, hacks, hacks...
class MyGradientSpan(val grdWidth: Float, color: Int, highlightColor: Int) : CharacterStyle(), UpdateAppearance {
class MyGradientSpan(val gradientWidth: Float, color: Int, highlightColor: Int) : CharacterStyle(), UpdateAppearance {
private val matrix = Matrix()
private var shader = LinearGradient(
0f, 50f, grdWidth, 50f,
private val shader = LinearGradient(
0f, 1f, gradientWidth, 1f,
highlightColor, color,
Shader.TileMode.CLAMP
)
Expand All @@ -28,13 +28,15 @@ class MyGradientSpan(val grdWidth: Float, color: Int, highlightColor: Int) : Cha
override fun updateDrawState(tp: TextPaint) {
tp.color = Color.WHITE
val o = 5 * ((lineCount / lineCountDivider) % (lineOffsets.size / 5))
val ourProgress = max(0f, min(1f, lerpInv(lineOffsets[o + 3].toFloat(), lineOffsets[o + 4]
val preOffsetFromLeft = lineOffsets[o].toFloat()
val ourProgressLtr = max(0f, min(1f, lerpInv(lineOffsets[o + 2].toFloat(), lineOffsets[o + 3]
.toFloat(), lerp(0f, totalCharsForProgress.toFloat(), progress))))
val ourProgress = if (lineOffsets[o + 4] == -1) 1f - ourProgressLtr else ourProgressLtr
shader.setLocalMatrix(matrix.apply {
reset()
postTranslate(lineOffsets[o].toFloat() + ((lineOffsets[o + 2]
+ (grdWidth * 3)) * ourProgress) - (grdWidth * 2), 0f)
postScale(1f, lineOffsets[o + 1] / 100f)
if (lineOffsets[o + 4] == -1) postRotate(180f, gradientWidth / 2f, 1f)
postTranslate(preOffsetFromLeft + (lineOffsets[o + 1] + gradientWidth * 2) * ourProgress
- gradientWidth * 2f, 0f)
})
tp.shader = shader
lineCount++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ 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.replaceAllSupport
import org.akanework.gramophone.logic.utils.SemanticLyrics.Word
import java.io.File
import java.nio.charset.Charset
import kotlin.math.min

object LrcUtils {

Expand Down Expand Up @@ -84,28 +86,30 @@ 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
if (line.lyric.words.isNullOrEmpty()) 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) {
// 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 }
if (wordsWithBarriers[wordIndex].isRtl != barrier.second)
wordsWithBarriers[wordIndex] = wordsWithBarriers[wordIndex].copy(isRtl = barrier.second)
wordsWithBarriers.replaceAllSupport(skipFirst = wordIndex) {
if (it.isRtl != barrier.second) it.copy(isRtl = barrier.second) else it }
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 {
val barrierTime = min(evilWord.timeRange.first + ((line.lyric.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.charRange.first))).toULong(), evilWord.timeRange.last - 1uL)
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class NewLyricsView(context: Context, attrs: AttributeSet) : View(context, attrs
private val translationTextSize = context.resources.getDimension(R.dimen.lyric_tl_text_size)
private val translationBackgroundTextSize = context.resources.getDimension(R.dimen.lyric_tl_bg_text_size)
private var colorSpanPool = mutableListOf<MyForegroundColorSpan>()
private val bounds = Rect()
private var spForRender: List<SbItem>? = null
private var spForMeasure: Pair<Pair<Int, Int>, List<SbItem>>? = null
private var lyrics: SemanticLyrics? = null
Expand Down Expand Up @@ -257,7 +256,8 @@ class NewLyricsView(context: Context, attrs: AttributeSet) : View(context, attrs
var gradientProgress = Float.NEGATIVE_INFINITY
val firstTs = lines?.get(i)?.lyric?.start ?: ULong.MIN_VALUE
val lastTs = lines?.get(i)?.lyric?.words?.lastOrNull()?.timeRange?.last ?: lines
?.find { it.lyric.start > lines[i].lyric.start }?.lyric?.start ?: Long.MAX_VALUE.toULong()
?.find { it.lyric.start > lines[i].lyric.start }?.lyric?.start?.minus(1uL) ?:
Long.MAX_VALUE.toULong()
val timeOffsetForUse = min(scaleInAnimTime, min(lerp(firstTs.toFloat(), lastTs.toFloat(),
0.5f) - firstTs.toFloat(), firstTs.toFloat()))
val highlight = posForRender >= firstTs - timeOffsetForUse.toULong() &&
Expand Down Expand Up @@ -462,37 +462,31 @@ class NewLyricsView(context: Context, attrs: AttributeSet) : View(context, attrs
}, (width * smallSizeFactor).toInt()).setAlignment(align).build()
SbItem(layout, sb, paddingTop.dpToPx(context), paddingBottom.dpToPx(context),
syncedLines?.get(i)?.lyric?.words?.map {
val isRtl = layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT
val alignmentOpposite = if (isRtl) layout.alignment == Layout.Alignment.ALIGN_NORMAL
else layout.alignment == Layout.Alignment.ALIGN_OPPOSITE
val ia = mutableListOf<Int>()
val firstLine = layout.getLineForOffset(it.charRange.first)
val lastLine = layout.getLineForOffset(it.charRange.last + 1)
for (line in firstLine..lastLine) {
var baseOffset = if (line == firstLine && !alignmentOpposite) {
val il = StaticLayoutBuilderCompat.obtain(sb, layout.paint, Int.MAX_VALUE)
.setStart(layout.getLineStart(line)).setEnd(it.charRange.first)
.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))
.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))
.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
layout.paint.getTextBounds(sb.toString(), max(it.charRange.first, layout
.getLineStart(line)), min(it.charRange.last + 1, layout.getLineEnd(line)), bounds)
ia.add(bounds.width()) // width of text in this line
ia.add(max(it.charRange.first, layout.getLineStart(line)) - it.charRange.first) // prefix chars
ia.add(min(layout.getLineEnd(line), it.charRange.last + 1) - it.charRange.first) // suffix chars
val firstInLine = max(it.charRange.first, layout.getLineStart(line))
val lastInLineExcl = min(it.charRange.last + 1, layout.getLineEnd(line))
val horizontalLeft = if (!it.isRtl && layout.getParagraphDirection(0) != Layout.DIR_RIGHT_TO_LEFT)
layout.getPrimaryHorizontal(firstInLine)
else if (!it.isRtl)
layout.getSecondaryHorizontal(firstInLine)
else if (layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT)
layout.getPrimaryHorizontal(lastInLineExcl - 1)
else layout.getSecondaryHorizontal(lastInLineExcl - 1)
val horizontalRight = if (!it.isRtl && layout.getParagraphDirection(0) != Layout.DIR_RIGHT_TO_LEFT)
layout.getPrimaryHorizontal(lastInLineExcl - 1)
else if (!it.isRtl)
layout.getSecondaryHorizontal(lastInLineExcl - 1)
else if (layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT)
layout.getPrimaryHorizontal(firstInLine)
else layout.getSecondaryHorizontal(firstInLine)
ia.add(horizontalLeft.toInt()) // offset from left to start of word
ia.add((horizontalRight - horizontalLeft).toInt()) // width of text in this line
ia.add(firstInLine - it.charRange.first) // prefix chars
ia.add((lastInLineExcl - 1) - it.charRange.first) // suffix chars
ia.add(if (it.isRtl) -1 else 1)
}
return@map ia
})
Expand Down

0 comments on commit 511c5d0

Please sign in to comment.