end of lyricks and misic generation

This commit is contained in:
Thomas Demirdjian
2025-08-26 13:45:50 +02:00
parent 2a34e9d547
commit 0ca7544005
14 changed files with 641 additions and 375 deletions
+240 -41
View File
@@ -1,22 +1,178 @@
import React, { useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import Page from "../../layouts/Page";
import { background, icons } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import Slider from "../../components/Slider";
import { Image, Platform, Pressable, View } from "react-native";
import { Image, Platform, Pressable, View, Text } from "react-native";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { BlurView } from "expo-blur";
import { Style } from "../../styles";
import { responsiveWidth } from "react-native-responsive-dimensions";
import { gutters, size } from "../../styles/Style";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import ValidateModal from "../../components/modal/ValidateModal";
import { useRoute } from "@react-navigation/core";
import firebase from "../../config/firebase";
import { useAudioPlayer } from "expo-audio";
import { responsiveHeight } from "react-native-responsive-dimensions";
const SongReady = () => {
const params = useRoute().params || {};
const projectId = params?.projectId;
const [showValidateModal, setShowValidateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState({ 0: false, 1: false });
const [progressInfo, setProgressInfo] = useState({
0: { pos: 0, dur: 0 },
1: { pos: 0, dur: 0 },
});
const player0 = useAudioPlayer(
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
);
const player1 = useAudioPlayer(
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
);
// Charger les URLs depuis le document projet
useEffect(() => {
if (!projectId) return;
const unsub = firebase
.firestore()
.collection("projects")
.doc(projectId)
.onSnapshot((doc) => {
const data = doc.data() || {};
const urls = Array.isArray(data?.musicUrls)
? data.musicUrls.slice(0, 2)
: [];
setMusicUrls(urls);
});
return () => unsub?.();
}, [projectId]);
// Sync progression depuis les players
useEffect(() => {
const id = setInterval(() => {
const d0 = (player0?.duration || 0) * 1000;
const p0 = (player0?.currentTime || 0) * 1000;
const d1 = (player1?.duration || 0) * 1000;
const p1 = (player1?.currentTime || 0) * 1000;
setProgressInfo({ 0: { pos: p0, dur: d0 }, 1: { pos: p1, dur: d1 } });
setIsPlaying({ 0: !!player0?.playing, 1: !!player1?.playing });
}, 300);
return () => clearInterval(id);
}, [player0, player1]);
const togglePlay = async (idx) => {
const url = musicUrls[idx];
if (!url) return;
try {
// Pause l'autre piste si elle joue
const other = idx === 0 ? 1 : 0;
if (isPlaying[other]) {
if (other === 0) await player0?.pause?.();
else await player1?.pause?.();
setIsPlaying((p) => ({ ...p, [other]: false }));
}
const player = idx === 0 ? player0 : player1;
if (!player) return;
if (player.playing) {
await player.pause?.();
setIsPlaying((p) => ({ ...p, [idx]: false }));
} else {
await player.play?.();
setIsPlaying((p) => ({ ...p, [idx]: true }));
}
} catch (e) {
console.log("Audio error", e?.message);
}
};
const fmt = (ms) => {
const total = Math.max(0, Math.floor((ms || 0) / 1000));
const m = Math.floor(total / 60)
.toString()
.padStart(1, "0");
const s = (total % 60).toString().padStart(2, "0");
return `${m}:${s}`;
};
const onSeek = async (idx, ratio) => {
try {
const info = progressInfo[idx] || {};
const dur = info.dur || 0;
const pos = Math.floor(dur * ratio);
const player = idx === 0 ? player0 : player1;
if (player && dur > 0) {
await player.seekTo?.(Math.floor((pos || 0) / 1000));
}
} catch (e) {
console.log("Seek error", e?.message);
}
};
const validateSelection = async () => {
try {
const url = musicUrls[selectedIndex];
if (!projectId || !url) return;
await firebase
.firestore()
.collection("projects")
.doc(projectId)
.set(
{
song: { index: selectedIndex, url },
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
navigate(Routes.PouchReady);
} catch (e) {
console.log("Validate error", e?.message);
}
};
const onPressRegenerate = async () => {
try {
if (!projectId) return goBack();
const doc = await firebase
.firestore()
.collection("projects")
.doc(projectId)
.get();
const project = doc.data() || {};
const musicConfig = project?.musicConfig || {};
const callable = firebase
.functions()
.httpsCallable("music-generateMusic");
const { data } = await callable({
...musicConfig,
projectId,
});
const taskId =
data?.response?.data?.taskId || data?.response?.data?.task_id;
if (taskId) {
await firebase.firestore().collection("projects").doc(projectId).set(
{
sunoTaskId: taskId,
musicStatus: "GENERATING",
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
}
} catch (e) {
console.log("Regenerate error", e?.message);
} finally {
goBack();
}
};
return (
<Page headerType="NONE" backgroundImg={background.studioBG2}>
@@ -25,38 +181,84 @@ const SongReady = () => {
<CreateLyricsHeader
title="Ta chanson est prête !"
subTitle="Quen penses-tu ?"
/>
<View
style={{
...size({ size: responsiveWidth(80) }),
alignSelf: "center",
borderRadius: 1000,
overflow: "hidden",
marginTop: 16,
containerStyle={{
marginBottom: responsiveHeight(2),
}}
>
<BlurView
intensity={40}
tint="dark"
style={{ ...Style.containerCenter, flex: 1 }}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<Image source={icons.disk} />
</BlurView>
</View>
<View style={{ marginTop: 49 }}>
<Slider value="0" maxValue="2:11" />
<Pressable
style={{
alignSelf: "center",
...size({ size: 48 }),
...Style.containerCenter,
}}
>
<Image source={icons.play} />
</Pressable>
/>
<View style={{ gap: 16 }}>
{[0, 1].map((idx) => (
<BlurView
key={idx}
intensity={40}
tint="dark"
style={{
borderRadius: 16,
overflow: "hidden",
padding: 12,
backgroundColor: "#FFFFFF0A",
}}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
>
<Pressable
onPress={() => togglePlay(idx)}
style={{ ...Style.containerCenter, ...size({ size: 48 }) }}
>
<Image source={isPlaying[idx] ? icons.pause : icons.play} />
</Pressable>
<View style={{ flex: 1 }}>
<Text
style={{
color: "white",
marginBottom: responsiveHeight(1),
}}
>{`Morceau ${idx + 1}`}</Text>
<Slider
value={fmt(progressInfo[idx]?.pos)}
maxValue={fmt(progressInfo[idx]?.dur)}
progress={
progressInfo[idx]?.dur
? (progressInfo[idx].pos || 0) / progressInfo[idx].dur
: 0
}
seekEnabled={!!musicUrls[idx]}
onSeek={(ratio) => onSeek(idx, ratio)}
/>
</View>
<Pressable
onPress={() => setSelectedIndex(idx)}
style={{ ...Style.containerCenter, ...size({ size: 24 }) }}
>
<View
style={{
width: 18,
height: 18,
borderRadius: 9,
borderWidth: 2,
borderColor: "white",
alignItems: "center",
justifyContent: "center",
}}
>
{selectedIndex === idx && (
<View
style={{
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: "white",
}}
/>
)}
</View>
</Pressable>
</View>
</BlurView>
))}
</View>
</View>
<View
@@ -70,21 +272,18 @@ const SongReady = () => {
<BorderGradientButton
title="Regénérer"
icon={icons.stars}
onPress={() =>
navigate(Routes.Regenerate, {
progress: 63,
})
}
onPress={onPressRegenerate}
/>
<GradientButton
title="Valider"
title="Choisir ce morceau"
onPress={() => setShowValidateModal(true)}
disabled={!musicUrls?.length}
/>
</View>
<ValidateModal
visible={showValidateModal}
onClose={() => setShowValidateModal(false)}
onPressValidate={() => navigate(Routes.PouchReady)}
onPressValidate={validateSelection}
/>
</Page>
);