This commit is contained in:
2025-09-03 11:32:56 +02:00
parent ade2d6ef9d
commit df880874e3
5 changed files with 297 additions and 60 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ const Playback = ({ route }) => {
return;
// return navigate(Routes.RecordPlayback, { project });
}
if (project?.[`musicTimestamps.${songIndex}`]) {
if (project?.musicTimestamps?.[songIndex]) {
console.log("exists");
return navigate(Routes.RecordPlayback, { project });
}
+104 -49
View File
@@ -12,6 +12,7 @@ import { Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Svg, { Circle } from "react-native-svg";
import GradientButton from "../../components/GradientButton";
import KaraokeLyrics from "../../components/KaraokeLyrics";
import MusicLandHeader from "../../components/MusicLandHeader";
import { increment, projectsRef } from "../../config/firebase";
import { Routes } from "../../navigation";
@@ -60,15 +61,67 @@ const RecordPlayback = ({ route }) => {
}, [songIndex]);
const alignedWords = useMemo(() => {
const ts = project?.[`musicTimestamps.${songIndex}`];
null;
const ts = project?.musicTimestamps?.[musicIndex];
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
return arr.map((w) => ({
word: String(w?.word ?? "").replace(/\n/g, " "),
word: String(w?.word ?? ""),
startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0),
}));
}, [project?.musicTimestamps, musicIndex]);
// Build line groups to display line-by-line
const lines = useMemo(() => {
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());
for (let i = 0; i < alignedWords.length; i++) {
const w = alignedWords[i];
const original = String(w.word || "");
const textNoNewline = original.replace(/\n/g, " ");
const gapToNext =
i < alignedWords.length - 1
? Math.max(0, alignedWords[i + 1].startS - w.endS)
: 0;
// If buffer empty, mark start
if (buf.length === 0) start = w.startS;
// Section tag becomes its own line
if (isSectionTag(textNoNewline)) {
const joined = clean(textNoNewline);
if (joined)
out.push({ text: joined, startS: start ?? w.startS, endS: w.endS });
buf = [];
start = null;
continue;
}
buf.push(textNoNewline);
const eolByNewline = /\n/.test(original);
const eolByPause = gapToNext >= 0.6;
const eolByPunct = isSentenceEnd(textNoNewline);
const isLast = i === alignedWords.length - 1;
if (eolByNewline || eolByPause || eolByPunct || isLast) {
const joined = clean(buf.join(" "));
if (joined)
out.push({ text: joined, startS: start ?? w.startS, endS: w.endS });
buf = [];
start = null;
}
}
return out;
}, [alignedWords]);
// console.log("alignedWords:", alignedWords);
useEffect(() => {
listenedMsRef.current = 0;
@@ -88,18 +141,18 @@ const RecordPlayback = ({ route }) => {
return () => clearInterval(id);
}, [player]);
// Compute current word index from playback time
// Compute current line index from playback time
const currentTimeS = (progressInfo?.pos || 0) / 1000;
const currentIdx = useMemo(() => {
if (!alignedWords || alignedWords.length === 0) return -1;
for (let i = 0; i < alignedWords.length; i++) {
const w = alignedWords[i];
if (currentTimeS >= w.startS && currentTimeS <= w.endS) return i;
const currentLineIdx = useMemo(() => {
if (!lines || lines.length === 0) return -1;
for (let i = 0; i < lines.length; i++) {
const L = lines[i];
if (currentTimeS >= L.startS && currentTimeS <= L.endS) return i;
}
if (currentTimeS > (alignedWords[alignedWords.length - 1]?.endS || 0))
return alignedWords.length - 1;
if (currentTimeS > (lines[lines.length - 1]?.endS || 0))
return lines.length - 1;
return -1;
}, [alignedWords, currentTimeS]);
}, [lines, currentTimeS]);
// Permissions au mount + cleanup
useEffect(() => {
@@ -442,7 +495,10 @@ const RecordPlayback = ({ route }) => {
>
<CreateLyricsHeader>
{(isRecording || showProgress) && alignedWords?.length > 0 ? (
<KaraokeLine words={alignedWords} currentIdx={currentIdx} />
<KaraokeLyrics
alignedWords={alignedWords}
currentTimeS={(progressInfo?.pos || 0) / 1000}
/>
) : (
<Text
style={{
@@ -494,42 +550,41 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
export default RecordPlayback;
// Simple karaoke line renderer
const KaraokeLine = ({ words = [], currentIdx = -1 }) => {
const windowSize = 8;
const start = Math.max(0, currentIdx - 2);
const end = Math.min(
words.length,
currentIdx >= 0 ? currentIdx + windowSize : windowSize
);
const slice = words.slice(start, end);
// Render previous/current/next line to reduce jumpiness
const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => {
const prev = currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : "";
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : "";
const next =
currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : "";
return (
<Text
style={{
color: Palette.white,
fontSize: 18,
lineHeight: 24,
textAlign: "center",
}}
>
{slice.map((w, i) => {
const idx = start + i;
const text = String(w.word || "").replace(/\s+/g, " ");
const isCurrent = idx === currentIdx;
return (
<Text
key={`${idx}-${text}`}
style={{
color: isCurrent ? Palette.white : "#FFFFFFAA",
fontFamily: isCurrent
? FONT_FAMILY.InterSemiBold
: FONT_FAMILY.InterMedium,
}}
>
{text + " "}
</Text>
);
})}
</Text>
<View style={{ gap: 4 }}>
{/* {prev ? (
<Text style={{ color: "#FFFFFF99", fontSize: 14, textAlign: "center", fontFamily: FONT_FAMILY.InterMedium }}>
{prev}
</Text>
) : null} */}
<Text
style={{
color: Palette.white,
fontSize: 18,
textAlign: "center",
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{curr}
</Text>
{next ? (
<Text
style={{
color: "#FFFFFF99",
fontSize: 14,
textAlign: "center",
fontFamily: FONT_FAMILY.InterMedium,
}}
>
{next}
</Text>
) : null}
</View>
);
};
+45 -9
View File
@@ -13,6 +13,7 @@ import { Image, Platform, Pressable, Text, View } from "react-native";
import Carousel from "react-native-reanimated-carousel";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { icons, img } from "../assets";
import KaraokeLyrics from "../components/KaraokeLyrics";
import {
arrayRemove,
arrayUnion,
@@ -163,6 +164,34 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
};
}, [audioPlayer, videoPlayer]);
// Build alignedWords from item musicTimestamps
const alignedWords = useMemo(() => {
const idx = Number(item?.songIndex) || 0;
const ts = item?.musicTimestamps?.[idx];
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
return arr.map((w) => ({
word: String(w?.word ?? ""),
startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0),
}));
}, [item?.musicTimestamps, item?.songIndex]);
// Track current time for lyrics sync
const [currentTimeS, setCurrentTimeS] = useState(0);
useEffect(() => {
if (!isActive) return;
const id = setInterval(() => {
try {
const t = hasExternalAudio
? Number(audioPlayer?.currentTime || 0)
: Number(videoPlayer?.currentTime || 0);
setCurrentTimeS(t);
} catch (e) {}
}, 250);
return () => clearInterval(id);
}, [isActive, hasExternalAudio, audioPlayer, videoPlayer]);
return (
<View
style={{
@@ -343,15 +372,22 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item?.title || "Description chanson"}
</Text>
{alignedWords?.length > 0 ? (
<KaraokeLyrics
alignedWords={alignedWords}
currentTimeS={currentTimeS}
/>
) : (
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item?.title || "Description chanson"}
</Text>
)}
</BlurView>
</View>
</View>