fix playback offset
This commit is contained in:
+92
-146
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user