karaoke lines vertical
This commit is contained in:
@@ -65,28 +65,216 @@ export function groupAlignedWordsToLines(alignedWords = [], { removeTags = true
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(' '))
|
||||||
|
if (!text) return
|
||||||
|
out.push({
|
||||||
|
text,
|
||||||
|
startS: start ?? Number(words[0]?.startS || 0),
|
||||||
|
endS: Number(endS || words[words.length - 1]?.endS || 0),
|
||||||
|
words,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (buf.length === 0) start = Number(w.startS || 0)
|
||||||
|
|
||||||
|
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),
|
||||||
|
words: [
|
||||||
|
{
|
||||||
|
text: joined,
|
||||||
|
startS: Number(w.startS || 0),
|
||||||
|
endS: Number(w.endS || 0),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf = []
|
||||||
|
start = null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clean(textNoTag)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const wordItem = {
|
||||||
|
text: textNoTag,
|
||||||
|
startS: Number(w.startS || 0),
|
||||||
|
endS: Number(w.endS || 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.push(wordItem)
|
||||||
|
|
||||||
|
const eolByNewline = /\n/.test(original)
|
||||||
|
const eolByPunct = isSentenceEnd(textNoTag)
|
||||||
|
const isLast = i === alignedWords.length - 1
|
||||||
|
|
||||||
|
if (buf.length && (eolByNewline || eolByPunct || isLast)) {
|
||||||
|
pushLine(buf, Number(w.endS || 0))
|
||||||
|
buf = []
|
||||||
|
start = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp01 = (value) => Math.max(0, Math.min(1, value))
|
||||||
|
|
||||||
|
const alphaWhite = (alpha) => `rgba(255,255,255,${alpha})`
|
||||||
|
|
||||||
export default function KaraokeLyrics({
|
export default function KaraokeLyrics({
|
||||||
alignedWords = [],
|
alignedWords = [],
|
||||||
currentTimeS = 0,
|
currentTimeS = 0,
|
||||||
showContext = true,
|
showContext = true,
|
||||||
removeTags = true,
|
removeTags = true,
|
||||||
|
mode = 'focus',
|
||||||
|
teleprompterLines = 6,
|
||||||
|
teleprompterFontSize = 18,
|
||||||
|
teleprompterLineHeight = 28,
|
||||||
|
teleprompterAnchorLine = 1,
|
||||||
}) {
|
}) {
|
||||||
const lines = useMemo(
|
const lines = useMemo(
|
||||||
() => groupAlignedWordsToLines(alignedWords, { removeTags }),
|
() => groupAlignedWordsToLines(alignedWords, { removeTags }),
|
||||||
[alignedWords, removeTags]
|
[alignedWords, removeTags]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const teleprompterData = useMemo(
|
||||||
|
() => groupAlignedWordsToLineItems(alignedWords, { removeTags }),
|
||||||
|
[alignedWords, removeTags]
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeLines = mode === 'teleprompter' ? teleprompterData : lines
|
||||||
|
|
||||||
const currentLineIdx = useMemo(() => {
|
const currentLineIdx = useMemo(() => {
|
||||||
if (!lines || lines.length === 0) return -1
|
if (!activeLines || activeLines.length === 0) return -1
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < activeLines.length; i++) {
|
||||||
const L = lines[i]
|
const L = activeLines[i]
|
||||||
if (currentTimeS >= (L.startS || 0) && currentTimeS <= (L.endS || 0)) return i
|
if (currentTimeS >= (L.startS || 0) && currentTimeS <= (L.endS || 0)) return i
|
||||||
}
|
}
|
||||||
if (currentTimeS > (lines[lines.length - 1]?.endS || 0)) return lines.length - 1
|
if (currentTimeS > (activeLines[activeLines.length - 1]?.endS || 0))
|
||||||
|
return activeLines.length - 1
|
||||||
return -1
|
return -1
|
||||||
}, [lines, currentTimeS])
|
}, [activeLines, currentTimeS])
|
||||||
|
|
||||||
if (!lines.length) return null
|
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
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
height: containerHeight,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
transform: [{ translateY }],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeLines.map((line, idx) => {
|
||||||
|
const isUpcoming = currentTimeS < (line.startS || 0)
|
||||||
|
const isPast = currentTimeS > (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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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((currentTimeS - (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>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const prev = showContext && currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ''
|
const prev = showContext && currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ''
|
||||||
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : lines[0]?.text
|
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : lines[0]?.text
|
||||||
|
|||||||
@@ -1090,7 +1090,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: bottom + 200,
|
top: top + 90,
|
||||||
paddingHorizontal: gutters,
|
paddingHorizontal: gutters,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1099,6 +1099,9 @@ const RecordPlayback = ({ route }) => {
|
|||||||
<KaraokeLyrics
|
<KaraokeLyrics
|
||||||
alignedWords={alignedWords}
|
alignedWords={alignedWords}
|
||||||
currentTimeS={(progressInfo?.pos || 0) / 1000}
|
currentTimeS={(progressInfo?.pos || 0) / 1000}
|
||||||
|
mode="teleprompter"
|
||||||
|
teleprompterLines={6}
|
||||||
|
teleprompterAnchorLine={1}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -999,13 +999,19 @@ const RecordPlayback = ({ route }) => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: bottom + 180,
|
top: top + 90,
|
||||||
paddingHorizontal: gutters,
|
paddingHorizontal: gutters,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CreateLyricsHeader>
|
<CreateLyricsHeader>
|
||||||
{isRecording && alignedWords.length > 0 ? (
|
{isRecording && alignedWords.length > 0 ? (
|
||||||
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={pos} />
|
<KaraokeLyrics
|
||||||
|
alignedWords={alignedWords}
|
||||||
|
currentTimeS={pos}
|
||||||
|
mode="teleprompter"
|
||||||
|
teleprompterLines={6}
|
||||||
|
teleprompterAnchorLine={1}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
Reference in New Issue
Block a user