Skip to content

Commit 6f9dd7c

Browse files
committed
closes #470
i was too lazy to do the width limiting in UI, but the parsing for it is done (ie TTML would width-limit both sides if v1 and v2 are there, but only right with v1 and v1000)
1 parent 1b30dd8 commit 6f9dd7c

File tree

3 files changed

+75
-15
lines changed

3 files changed

+75
-15
lines changed

app/src/main/java/org/akanework/gramophone/logic/utils/SemanticLyrics.kt

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,23 @@ private const val TAG = "SemanticLyrics"
4949
* We completely ignore all ID3 tags from the header as MediaStore is our source of truth.
5050
*/
5151

52+
// This does not encode the full range of possible information in the TTML format, only what's used
53+
// for actual rendering. This could change in the future, but for now it's like this.
5254
@Parcelize
5355
enum class SpeakerEntity(
54-
val isVoice2: Boolean = false,
55-
val isGroup: Boolean = false,
56-
val isBackground: Boolean = false
56+
val isVoice2: Boolean = false, // align opposite
57+
val isGroup: Boolean = false, // center
58+
val isBackground: Boolean = false, // small
59+
val isWidthLimited: Boolean = false,
5760
) : Parcelable {
58-
Voice1,
59-
Background(isBackground = true),
60-
Voice2(isVoice2 = true),
61-
Voice2Background(isVoice2 = true, isBackground = true),
61+
// Voice is used if there should be no width limit as it's the only person, Voice1 is used if
62+
// there are more and hence width limit should be applied.
63+
Voice,
64+
VoiceBackground(isBackground = true),
65+
Voice1(isWidthLimited = true),
66+
Voice1Background(isWidthLimited = true, isBackground = true),
67+
Voice2(isWidthLimited = true, isVoice2 = true),
68+
Voice2Background(isWidthLimited = true, isVoice2 = true, isBackground = true),
6269
Group(isGroup = true),
6370
GroupBackground(isGroup = true, isBackground = true)
6471
}
@@ -144,7 +151,9 @@ private sealed class SyntacticLrc {
144151
// but hey, we tried. Can't do much about it.
145152
// If you want to write something that looks like a timestamp into your lyrics,
146153
// you'll probably have to delete the following three lines.
147-
if (!(out.lastOrNull() is NewLine? || out.lastOrNull() is SyncPoint))
154+
val lastOrNull = out.lastOrNull()
155+
if (!(lastOrNull is NewLine? || lastOrNull is SyncPoint
156+
|| (lastOrNull is SpeakerTag && lastOrNull.speaker.isBackground)))
148157
out.add(NewLine.SyntheticNewLine())
149158
out.add(SyncPoint(parseTime(tmMatch)))
150159
pos += tmMatch.value.length
@@ -212,7 +221,7 @@ private sealed class SyntacticLrc {
212221
when {
213222
lastSpeaker?.isGroup == true -> SpeakerEntity.GroupBackground
214223
lastSpeaker?.isVoice2 == true -> SpeakerEntity.Voice2Background
215-
else -> SpeakerEntity.Background
224+
else -> SpeakerEntity.Voice1Background
216225
}
217226
)
218227
)
@@ -518,6 +527,7 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
518527
var lastSyncPoint: ULong? = null
519528
var lastWordSyncPoint: ULong? = null
520529
var speaker: SpeakerEntity? = null
530+
var hadVoice2 = false
521531
var hadLyricSinceWordSync = true
522532
var hadWordSyncSinceNewLine = false
523533
val currentLine = mutableListOf<Pair<ULong, String?>>()
@@ -547,6 +557,9 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
547557

548558
is SyntacticLrc.SpeakerTag -> {
549559
speaker = element.speaker
560+
if (element.speaker.isVoice2) {
561+
hadVoice2 = true
562+
}
550563
}
551564

552565
is SyntacticLrc.WordSyncPoint -> {
@@ -683,6 +696,13 @@ fun parseLrc(lyricText: String, trimEnabled: Boolean, multiLineEnabled: Boolean)
683696
out.sortBy { it.start }
684697
var previousLyric: LyricLine? = null
685698
out.forEach { lyric ->
699+
if (!hadVoice2) { // If there is no v2: tag, we should avoid width limit.
700+
lyric.speaker = when (lyric.speaker) {
701+
SpeakerEntity.Voice1 -> SpeakerEntity.Voice
702+
SpeakerEntity.Voice1Background -> SpeakerEntity.VoiceBackground
703+
else -> lyric.speaker
704+
}
705+
}
686706
val mainEnd = if (lyric.start == previousLyric?.start) out.firstOrNull {
687707
it.start == lyric.start && !it.endIsImplicit
688708
}?.end else null
@@ -1501,22 +1521,28 @@ fun parseTtml(audioMimeType: String?, lyricText: String): SemanticLyrics? {
15011521
} while (cur != -1)
15021522
out
15031523
}
1524+
val hasAtLeastTwoPeople = people["person"]?.let { it.size > 1 } == true
15041525
if (paragraphs.find { it.time != null } == null) {
15051526
return UnsyncedLyrics(paragraphs.map {
15061527
val text = it.texts.joinToString("") { it.text }
15071528
val isBg = it.role == "x-bg"
15081529
val isGroup = peopleToType[it.agent] == "group"
1530+
val isOther = peopleToType[it.agent] == "other"
1531+
// first person goes left, second right, third left, fourth right, and so on.
1532+
// and the same goes for "other" except that we start on the right here.
15091533
val isVoice2 =
15101534
it.agent != null && (people[peopleToType[it.agent]] ?: throw NullPointerException(
15111535
"expected to find ${it.agent} (${peopleToType[it.agent]}) in $people"
1512-
)).indexOf(it.agent) % 2 == 1
1536+
)).indexOf(it.agent) % 2 == (if (isOther) 0 else 1)
15131537
val speaker = when {
15141538
isGroup && isBg -> SpeakerEntity.GroupBackground
15151539
isGroup -> SpeakerEntity.Group
15161540
isVoice2 && isBg -> SpeakerEntity.Voice2Background
15171541
isVoice2 -> SpeakerEntity.Voice2
1518-
isBg -> SpeakerEntity.Background
1519-
else -> SpeakerEntity.Voice1
1542+
hasAtLeastTwoPeople && isBg -> SpeakerEntity.Voice1Background
1543+
hasAtLeastTwoPeople -> SpeakerEntity.Voice1
1544+
isBg -> SpeakerEntity.VoiceBackground
1545+
else -> SpeakerEntity.Voice
15201546
}
15211547
Pair(text, speaker)
15221548
})
@@ -1536,17 +1562,22 @@ fun parseTtml(audioMimeType: String?, lyricText: String): SemanticLyrics? {
15361562
?.toMutableList()
15371563
val isBg = it.role == "x-bg"
15381564
val isGroup = peopleToType[it.agent] == "group"
1565+
val isOther = peopleToType[it.agent] == "other"
1566+
// first person goes left, second right, third left, fourth right, and so on.
1567+
// and the same goes for "other" except that we start on the right here.
15391568
val isVoice2 =
15401569
it.agent != null && (people[peopleToType[it.agent]] ?: throw NullPointerException(
15411570
"expected to find ${it.agent} (${peopleToType[it.agent]}) in $people"
1542-
)).indexOf(it.agent) % 2 == 1
1571+
)).indexOf(it.agent) % 2 == (if (isOther) 0 else 1)
15431572
val speaker = when {
15441573
isGroup && isBg -> SpeakerEntity.GroupBackground
15451574
isGroup -> SpeakerEntity.Group
15461575
isVoice2 && isBg -> SpeakerEntity.Voice2Background
15471576
isVoice2 -> SpeakerEntity.Voice2
1548-
isBg -> SpeakerEntity.Background
1549-
else -> SpeakerEntity.Voice1
1577+
hasAtLeastTwoPeople && isBg -> SpeakerEntity.Voice1Background
1578+
hasAtLeastTwoPeople -> SpeakerEntity.Voice1
1579+
isBg -> SpeakerEntity.VoiceBackground
1580+
else -> SpeakerEntity.Voice
15501581
}
15511582
if (it.time == null) {
15521583
throw IllegalArgumentException("it.time == null but some other P has non-null time")

app/src/main/java/org/akanework/gramophone/ui/components/NewLyricsView.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,8 @@ class NewLyricsView(context: Context, attrs: AttributeSet?) : ScrollingView2(con
531531
else Layout.Alignment.ALIGN_NORMAL
532532
val tl = syncedLine?.isTranslated == true
533533
val bg = speaker?.isBackground == true
534+
// TODO: width limiting to 85% if there is >1 singer
535+
//val widthLimit = speaker?.isWidthLimited == true
534536
val paddingTop = if (tl) 2 else 18
535537
val paddingBottom = if (i + 1 < (syncedLines?.size ?: -1) &&
536538
syncedLines?.get(i + 1)?.isTranslated == true

app/src/test/java/org/akanework/gramophone/LrcUtilsTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.akanework.gramophone
33
import androidx.media3.common.MimeTypes
44
import org.akanework.gramophone.logic.utils.LrcUtils
55
import org.akanework.gramophone.logic.utils.SemanticLyrics
6+
import org.akanework.gramophone.logic.utils.SpeakerEntity
67
import org.junit.Assert.assertEquals
78
import org.junit.Assert.assertFalse
89
import org.junit.Assert.assertNotEquals
@@ -462,6 +463,32 @@ class LrcUtilsTest {
462463
assert(!lrc[1].isTranslated)
463464
}
464465

466+
@Test
467+
fun voiceInsteadOfVoice1WhenNoVoice2() {
468+
val lrc = parseSynced("[00:00.00]v1:hello\n[bg:[00:01.00]bye]")
469+
assertNotNull(lrc)
470+
assertEquals(2, lrc!!.size)
471+
assertEquals("hello", lrc[0].text)
472+
assertEquals("bye", lrc[1].text)
473+
assertEquals(SpeakerEntity.Voice, lrc[0].speaker)
474+
assertEquals(SpeakerEntity.VoiceBackground, lrc[1].speaker)
475+
}
476+
477+
@Test
478+
fun voice1WhenThereIsVoice2() {
479+
val lrc = parseSynced("[00:00.00]v1:hello\n[bg:[00:01.00]bye]\n[00:02.00]v2:hello\n[bg:[00:03.00]bye]")
480+
assertNotNull(lrc)
481+
assertEquals(4, lrc!!.size)
482+
assertEquals("hello", lrc[0].text)
483+
assertEquals("bye", lrc[1].text)
484+
assertEquals("hello", lrc[2].text)
485+
assertEquals("bye", lrc[3].text)
486+
assertEquals(SpeakerEntity.Voice1, lrc[0].speaker)
487+
assertEquals(SpeakerEntity.Voice1Background, lrc[1].speaker)
488+
assertEquals(SpeakerEntity.Voice2, lrc[2].speaker)
489+
assertEquals(SpeakerEntity.Voice2Background, lrc[3].speaker)
490+
}
491+
465492
@Test
466493
fun explicitEndFromWordRecognized() {
467494
val lrc = parseSynced("[00:00.00][00:10.00]<00:01.00>hello<00:02.00><00:03.00>")

0 commit comments

Comments
 (0)