fix playback offset

This commit is contained in:
Thomas Demirdjian
2026-04-16 15:19:39 +02:00
parent 5c2bdc593c
commit 5a6833c1a2
11 changed files with 623 additions and 274 deletions
+92 -146
View File
@@ -3,84 +3,57 @@ import { Text, View } from 'react-native'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
export function groupAlignedWordsToLines(alignedWords = [], { removeTags = true } = {}) {
const out = []
let buf = []
let start = null
const clean = (txt) =>
String(txt || '')
.replace(/\s+/g, ' ')
.trim()
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || '').trim())
const isSectionTag = (txt) => /^\s*\[[^\]]+\]\s*$/i.test((txt || '').trim())
const SECTION_TAG_REGEX = /^\s*\[[^\]]+\]\s*$/i
const SECTION_TAG_LEADING_REGEX = /^\s*\[[^\]]+\]\s*/i
for (let i = 0; i < alignedWords.length; i++) {
const w = alignedWords[i] || {}
const original = String(w.word || '')
const textNoNewline = original.replace(/\n/g, ' ')
const textNoTag = removeTags ? textNoNewline.replace(/^\s*\[[^\]]+\]\s*/i, '') : textNoNewline
const next = alignedWords[i + 1] || null
const gapToNext = next ? Math.max(0, Number(next.startS || 0) - Number(w.endS || 0)) : 0
const cleanLyricsText = (txt) =>
String(txt || '')
.replace(/\s+/g, ' ')
.trim()
if (buf.length === 0) start = Number(w.startS || 0)
const isSentenceEnd = (txt) => /[.!?]$/.test(String(txt || '').trim())
if (isSectionTag(textNoNewline)) {
if (!removeTags) {
const joined = clean(textNoNewline)
if (joined)
out.push({
text: joined,
startS: start ?? Number(w.startS || 0),
endS: Number(w.endS || 0),
})
}
buf = []
start = null
continue
}
const isSectionTag = (txt) => SECTION_TAG_REGEX.test(String(txt || '').trim())
if (!clean(textNoTag)) {
continue
}
const findFocusedLineIndex = (lines = [], timeS = 0) => {
if (!lines.length) return -1
if (timeS <= (lines[0]?.startS || 0)) return 0
buf.push(textNoTag)
const eolByNewline = /\n/.test(original)
const eolByPause = gapToNext >= 0.6 // threshold for a logical break
const eolByPunct = isSentenceEnd(textNoTag)
const isLast = i === alignedWords.length - 1
if (eolByNewline || eolByPause || eolByPunct || isLast) {
const joined = clean(buf.join(' '))
if (joined)
out.push({
text: joined,
startS: start ?? Number(w.startS || 0),
endS: Number(w.endS || 0),
})
buf = []
start = null
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (timeS < (line.startS || 0)) return Math.max(0, i - 1)
if (timeS >= (line.startS || 0) && timeS <= (line.endS || 0)) return i
}
return out
return lines.length - 1
}
export function groupAlignedWordsToLineItems(
alignedWords = [],
{ removeTags = true } = {}
) {
const findFirstNonPastLineIndex = (lines = [], timeS = 0) => {
for (let i = 0; i < lines.length; i++) {
if (timeS <= Number(lines[i]?.endS || 0)) return i
}
return -1
}
export function groupAlignedWordsToLines(alignedWords = [], { removeTags = true } = {}) {
return groupAlignedWordsToLineItems(alignedWords, { removeTags }).map(
({ text, startS, endS }) => ({
text,
startS,
endS,
})
)
}
export function groupAlignedWordsToLineItems(alignedWords = [], { removeTags = true } = {}) {
const out = []
let buf = []
let start = null
const clean = (txt) =>
String(txt || '')
.replace(/\s+/g, ' ')
.trim()
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || '').trim())
const isSectionTag = (txt) => /^\s*\[[^\]]+\]\s*$/i.test((txt || '').trim())
const pushLine = (words, endS) => {
const text = clean(words.map((item) => item.text).join(' '))
const text = cleanLyricsText(words.map((item) => item.text).join(' '))
if (!text) return
out.push({
text,
startS: start ?? Number(words[0]?.startS || 0),
@@ -93,14 +66,17 @@ export function groupAlignedWordsToLineItems(
const w = alignedWords[i] || {}
const original = String(w.word || '')
const textNoNewline = original.replace(/\n/g, ' ')
const textNoTag = removeTags ? textNoNewline.replace(/^\s*\[[^\]]+\]\s*/i, '') : textNoNewline
const textNoTag = removeTags
? textNoNewline.replace(SECTION_TAG_LEADING_REGEX, '')
: textNoNewline
const next = alignedWords[i + 1] || null
const gapToNext = next ? Math.max(0, Number(next.startS || 0) - Number(w.endS || 0)) : 0
if (buf.length === 0) start = Number(w.startS || 0)
if (isSectionTag(textNoNewline)) {
if (!removeTags) {
const joined = clean(textNoNewline)
const joined = cleanLyricsText(textNoNewline)
if (joined) {
out.push({
text: joined,
@@ -121,17 +97,15 @@ export function groupAlignedWordsToLineItems(
continue
}
if (!clean(textNoTag)) {
if (!cleanLyricsText(textNoTag)) {
continue
}
const wordItem = {
buf.push({
text: textNoTag,
startS: Number(w.startS || 0),
endS: Number(w.endS || 0),
}
buf.push(wordItem)
})
const eolByNewline = /\n/.test(original)
const eolByPause = gapToNext >= 0.6 // threshold for a logical break
@@ -180,106 +154,78 @@ export default function KaraokeLyrics({
)
const currentLineIdx = useMemo(() => {
if (!activeLines || activeLines.length === 0) return -1
if (effectiveTimeS <= (activeLines[0]?.startS || 0)) return 0
for (let i = 0; i < activeLines.length; i++) {
const L = activeLines[i]
if (effectiveTimeS < (L.startS || 0)) return Math.max(0, i - 1)
if (effectiveTimeS >= (L.startS || 0) && effectiveTimeS <= (L.endS || 0)) return i
}
return activeLines.length - 1
return findFocusedLineIndex(activeLines, effectiveTimeS)
}, [activeLines, effectiveTimeS])
if (!activeLines.length) return null
if (mode === 'teleprompter') {
const safeLines = Math.max(1, teleprompterLines)
const containerHeight = teleprompterLineHeight * safeLines
const anchorLine = Math.max(0, Math.min(safeLines - 1, teleprompterAnchorLine))
const anchorOffset = teleprompterLineHeight * anchorLine
const safeIdx = Math.max(0, currentLineIdx)
const translateY = anchorOffset - safeIdx * teleprompterLineHeight
const inactiveAlpha = 0.35
const completedAlpha = 0.75
const activeAlpha = 1
const teleprompterLineIdx = findFirstNonPastLineIndex(teleprompterData, effectiveTimeS)
const visibleLines =
teleprompterLineIdx >= 0
? teleprompterData.slice(teleprompterLineIdx, teleprompterLineIdx + 2)
: []
if (!visibleLines.length) return null
return (
<View
style={{
height: containerHeight,
overflow: 'hidden',
gap: 6,
}}
>
<View
style={{
transform: [{ translateY }],
}}
>
{activeLines.map((line, idx) => {
const isUpcoming = effectiveTimeS < (line.startS || 0)
const isPast = effectiveTimeS > (line.endS || 0)
const lineKey = `${line.startS}-${idx}`
if (isUpcoming) {
return (
<Text
key={lineKey}
style={{
color: alphaWhite(inactiveAlpha),
fontSize: teleprompterFontSize,
lineHeight: teleprompterLineHeight,
textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{line.text}
</Text>
)
}
if (isPast) {
return (
<Text
key={lineKey}
style={{
color: alphaWhite(completedAlpha),
fontSize: teleprompterFontSize,
lineHeight: teleprompterLineHeight,
textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{line.text}
</Text>
)
}
{visibleLines.map((line, idx) => {
const isTopLine = idx === 0
const isActive =
isTopLine &&
effectiveTimeS >= Number(line.startS || 0) &&
effectiveTimeS <= Number(line.endS || 0)
const lineKey = `${line.startS}-${teleprompterLineIdx + idx}`
if (!isActive) {
return (
<Text
key={lineKey}
style={{
color: alphaWhite(inactiveAlpha),
fontSize: teleprompterFontSize,
lineHeight: teleprompterLineHeight,
textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{line.words.map((word, wordIndex) => {
const duration = Math.max(0.001, (word.endS || 0) - (word.startS || 0))
const progress = clamp01((effectiveTimeS - (word.startS || 0)) / duration)
const alpha = inactiveAlpha + (activeAlpha - inactiveAlpha) * progress
const text = word.text
return (
<Text key={`${lineKey}-${wordIndex}`} style={{ color: alphaWhite(alpha) }}>
{text}
{wordIndex < line.words.length - 1 ? ' ' : ''}
</Text>
)
})}
{line.text}
</Text>
)
})}
</View>
}
return (
<Text
key={lineKey}
style={{
fontSize: teleprompterFontSize,
lineHeight: teleprompterLineHeight,
textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{line.words.map((word, wordIndex) => {
const duration = Math.max(0.001, (word.endS || 0) - (word.startS || 0))
const progress = clamp01((effectiveTimeS - (word.startS || 0)) / duration)
const alpha = inactiveAlpha + (activeAlpha - inactiveAlpha) * progress
const text = word.text
return (
<Text key={`${lineKey}-${wordIndex}`} style={{ color: alphaWhite(alpha) }}>
{text}
{wordIndex < line.words.length - 1 ? ' ' : ''}
</Text>
)
})}
</Text>
)
})}
</View>
)
}