From 837a63499740baca72d8366239ba9a3ec8d8acba Mon Sep 17 00:00:00 2001 From: leon-morival Date: Tue, 21 Oct 2025 13:34:16 +0200 Subject: [PATCH] instrumental introduction --- src/screens/Library/MusicDetails.web.js | 21 ++++- src/screens/Writing/CreatingLyrics.js | 45 +++++++--- src/screens/Writing/CustomizeSongStructure.js | 53 +++++------- .../Writing/CustomizeSongStructure.web.js | 62 ++++++------- src/screens/Writing/Lyrics.js | 86 ++++++++++++++++--- src/utils/songStructure.js | 51 ++++++++++- 6 files changed, 226 insertions(+), 92 deletions(-) diff --git a/src/screens/Library/MusicDetails.web.js b/src/screens/Library/MusicDetails.web.js index 9de8558..b1a6435 100644 --- a/src/screens/Library/MusicDetails.web.js +++ b/src/screens/Library/MusicDetails.web.js @@ -39,8 +39,10 @@ import { FONT_FAMILY } from "../../styles/Fonts"; import Style, { gutters, size } from "../../styles/Style"; import { formatStructureLabel, + getPromptLabelForStructure, getSegmentMeta, normalizeStructureType, + segmentRequiresLyrics, } from "../../utils/songStructure"; // 20 secondes @@ -333,12 +335,20 @@ const MusicDetails = ({ route }) => { const counts = {}; return project.lyrics .map((s) => { - const body = (s?.lyrics || "").trim(); - if (!body) return null; const type = normalizeStructureType(s?.type); + const requiresLyrics = segmentRequiresLyrics(type); + const body = (s?.lyrics || "").trim(); + if (requiresLyrics && !body) return null; counts[type] = (counts[type] || 0) + 1; const label = formatStructureLabel(type, counts[type]); - return `[${label}]\n${body}`; + const displayLabel = + typeof label === "string" && label.length > 0 + ? label + : getPromptLabelForStructure(type); + if (!requiresLyrics) { + return `[${displayLabel}] — section instrumentale`; + } + return `[${displayLabel}]\n${body}`; }) .filter(Boolean) .join("\n\n"); @@ -404,8 +414,11 @@ const MusicDetails = ({ route }) => { const normalized = normalizeStructureType(type); const meta = getSegmentMeta(normalized); if (meta) { - return formatStructureLabel(normalized, index); + const label = formatStructureLabel(normalized, index); + if (label) return label; } + const prompt = getPromptLabelForStructure(normalized); + if (prompt) return prompt; if (normalized === "refrain") return index > 1 ? `Refrain ${index}` : "Refrain"; if (normalized === "couplet") diff --git a/src/screens/Writing/CreatingLyrics.js b/src/screens/Writing/CreatingLyrics.js index c902994..34989fe 100644 --- a/src/screens/Writing/CreatingLyrics.js +++ b/src/screens/Writing/CreatingLyrics.js @@ -12,7 +12,11 @@ import { navigate } from "../../navigation/NavigationService"; import { useUser } from "../../providers/UserDataProvider"; import { Palette, Style } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; -import { sanitizeStructureList } from "../../utils/songStructure"; +import { + normalizeStructureType, + sanitizeStructureList, + segmentRequiresLyrics, +} from "../../utils/songStructure"; const CreatingLyrics = ({ active, config, selections }) => { const [progress, setProgress] = useState(0); @@ -74,21 +78,40 @@ const CreatingLyrics = ({ active, config, selections }) => { }); const rawLyrics = Array.isArray(data?.lyrics) ? data.lyrics : []; const normalizedLyrics = rawLyrics.map((section) => { - const sanitizedType = - sanitizeStructureList([section?.type])[0] || section?.type || ""; + const sanitizedType = normalizeStructureType(section?.type); return { ...section, type: sanitizedType, + lyrics: section?.lyrics || "", }; }); - const alignedLyrics = - sanitizedStructure.length > 0 && - sanitizedStructure.length === normalizedLyrics.length - ? normalizedLyrics.map((section, index) => ({ - ...section, - type: sanitizedStructure[index], - })) - : normalizedLyrics; + + const remaining = [...normalizedLyrics]; + const takeForType = (type, { fallback = true } = {}) => { + const index = remaining.findIndex((item) => item.type === type); + if (index !== -1) { + return remaining.splice(index, 1)[0]; + } + if (!fallback) return null; + return remaining.shift() || null; + }; + + const structuredLyrics = sanitizedStructure.length + ? sanitizedStructure.map((rawType) => { + const type = normalizeStructureType(rawType); + if (!segmentRequiresLyrics(type)) { + const match = takeForType(type, { fallback: false }); + return { type, lyrics: match?.lyrics || "" }; + } + const match = takeForType(type, { fallback: true }); + return { + type, + lyrics: match?.lyrics || "", + }; + }) + : normalizedLyrics; + + const alignedLyrics = structuredLyrics.concat(remaining); const processedResult = { ...data, lyrics: alignedLyrics, diff --git a/src/screens/Writing/CustomizeSongStructure.js b/src/screens/Writing/CustomizeSongStructure.js index 104e1ca..a209367 100644 --- a/src/screens/Writing/CustomizeSongStructure.js +++ b/src/screens/Writing/CustomizeSongStructure.js @@ -23,15 +23,14 @@ import { import CreateLyricsHeader from "./components/CreateLyricsHeader"; const randomSuffix = () => Math.random().toString(36).slice(2, 8); -const createItemId = (type) => `${type}-${Date.now()}-${randomSuffix()}`; const createSortableItem = (type, index = 0) => ({ id: `${type}-${index}-${randomSuffix()}`, type, value: type, }); -const buildInitialItems = (structure = []) => - sanitizeStructureList(structure).map((type, index) => +const buildSortableItems = (values = []) => + sanitizeStructureList(values).map((type, index) => createSortableItem(type, index) ); @@ -42,24 +41,17 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { [baseStructure] ); const [sortableItems, setSortableItems] = useState(() => - buildInitialItems(sanitizedBase) + buildSortableItems(sanitizedBase) ); useEffect(() => { - setSortableItems((prev) => { - const prevTypes = prev.map((item) => item.type); - if ( - prevTypes.length === sanitizedBase.length && - prevTypes.every((type, index) => type === sanitizedBase[index]) - ) { - return prev; - } - return buildInitialItems(sanitizedBase); - }); + setSortableItems(buildSortableItems(sanitizedBase)); }, [sanitizedBase]); useEffect(() => { - const values = sortableItems.map((item) => item.type).filter(Boolean); + const values = sortableItems + .map((item) => item?.type || item?.value) + .filter(Boolean); onChange?.(values); }, [sortableItems, onChange]); @@ -99,20 +91,21 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { const toggleOptionalSegment = (type) => { setSortableItems((prev) => { - const existsIndex = prev.findIndex((item) => item.type === type); - if (existsIndex !== -1) { - return prev.filter((item) => item.type !== type); + const currentValues = prev.map((item) => item.type || item.value); + if (currentValues.includes(type)) { + const filtered = currentValues.filter((value) => value !== type); + return buildSortableItems(filtered); } const meta = getSegmentMeta(type); - let next = [...prev]; + let nextValues = [...currentValues]; if (meta?.exclusiveGroup) { - next = next.filter((item) => { - const itemMeta = getSegmentMeta(item.type || item.value); + nextValues = nextValues.filter((value) => { + const itemMeta = getSegmentMeta(value); return itemMeta?.exclusiveGroup !== meta.exclusiveGroup; }); } - next.push(createSortableItem(type, next.length)); - return next; + nextValues.push(type); + return buildSortableItems(nextValues); }); }; @@ -182,15 +175,11 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { customHandle scrollableRef={scrollRef} onDragEnd={({ data: newData }) => { - const next = (newData || []).map((it, index) => { - const nextType = it?.type || it?.value; - return { - id: it?.id || createItemId(nextType || `section-${index}`), - type: nextType, - value: nextType, - }; - }); - setSortableItems(next.filter((item) => item.type)); + const values = (newData || []) + .map((it) => it?.type || it?.value) + .filter(Boolean); + const sanitized = sanitizeStructureList(values); + setSortableItems(buildSortableItems(sanitized)); }} /> (typeof window !== "undefined" ? window.scrollY : 0); const randomSuffix = () => Math.random().toString(36).slice(2, 8); -const buildItems = (structure = []) => { +const buildSortableItems = (values = []) => { const counters = {}; - return structure.map((segment, index) => { + return sanitizeStructureList(values).map((segment, index) => { counters[segment] = (counters[segment] || 0) + 1; return { id: `${segment}-${index}-${randomSuffix()}`, @@ -44,7 +44,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { () => sanitizeStructureList(baseStructure), [baseStructure] ); - const [items, setItems] = React.useState(() => buildItems(sanitized)); + const [items, setItems] = React.useState(() => buildSortableItems(sanitized)); const [activeItemId, setActiveItemId] = React.useState(null); const containerRef = React.useRef(null); const dragStateRef = React.useRef({ @@ -54,38 +54,40 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { React.useEffect(() => { console.debug("[CustomizeSongStructure.web] sanitized", sanitized); - setItems((prev) => { - const prevTypes = prev.map((item) => item.type); - if (prevTypes.length === sanitized.length) { - const same = prevTypes.every((type, idx) => type === sanitized[idx]); - if (same) return prev; - } - return buildItems(sanitized); - }); + setItems(buildSortableItems(sanitized)); dragStateRef.current = { itemId: null, containerTop: 0 }; setActiveItemId(null); }, [sanitized]); React.useEffect(() => { console.debug("[CustomizeSongStructure.web] items", items); - onChange?.(items.map((item) => item.type)); + onChange?.( + items.map((item) => (item?.type || item?.value)).filter(Boolean) + ); }, [items, onChange]); const labeledItems = React.useMemo(() => { const counts = {}; return items.map((item) => { - counts[item.type] = (counts[item.type] || 0) + 1; + const type = item?.type || item?.value; + counts[type] = (counts[type] || 0) + 1; return { ...item, - label: formatStructureLabel(item.type, counts[item.type]), + type, + value: type, + label: formatStructureLabel(type, counts[type]), }; }); }, [items]); - const selectedTypes = React.useMemo( - () => new Set(items.map((item) => item.type)), - [items] - ); + const selectedTypes = React.useMemo(() => { + const set = new Set(); + items.forEach((item) => { + if (item?.type) set.add(item.type); + else if (item?.value) set.add(item.value); + }); + return set; + }, [items]); const optionalSegments = React.useMemo( () => @@ -98,24 +100,21 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { const toggleOptionalSegment = React.useCallback((type) => { setItems((prev) => { - const exists = prev.some((item) => item.type === type); - if (exists) { - return prev.filter((item) => item.type !== type); + const currentValues = prev.map((item) => item.type || item.value); + if (currentValues.includes(type)) { + const filtered = currentValues.filter((value) => value !== type); + return buildSortableItems(filtered); } const meta = getSegmentMeta(type); - let next = [...prev]; + let nextValues = [...currentValues]; if (meta?.exclusiveGroup) { - next = next.filter((item) => { - const itemMeta = getSegmentMeta(item.type); + nextValues = nextValues.filter((value) => { + const itemMeta = getSegmentMeta(value); return itemMeta?.exclusiveGroup !== meta.exclusiveGroup; }); } - next.push({ - id: `${type}-${Date.now()}-${randomSuffix()}`, - type, - value: type, - }); - return next; + nextValues.push(type); + return buildSortableItems(nextValues); }); }, []); @@ -169,6 +168,9 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { }; const handleRelease = () => { + setItems((prev) => + buildSortableItems(prev.map((item) => item.type || item.value)) + ); dragStateRef.current = { itemId: null, containerTop: 0, diff --git a/src/screens/Writing/Lyrics.js b/src/screens/Writing/Lyrics.js index 1ae48b0..b6dd3c8 100644 --- a/src/screens/Writing/Lyrics.js +++ b/src/screens/Writing/Lyrics.js @@ -17,9 +17,11 @@ import { Palette, gutters } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import { formatStructureLabel, + getPromptLabelForStructure, getSegmentMeta, normalizeStructureType, sanitizeStructureList, + segmentRequiresLyrics, } from "../../utils/songStructure"; import CustomInput from "./components/CustomInput"; @@ -52,21 +54,42 @@ const Lyrics = ({ navigation }) => { ? sanitizeStructureList(projectConfig.structure) : null; - if (normalizedSections.length) { - return { - title, - sections: normalizedSections, - }; + const structureToUse = + targetStructure && targetStructure.length + ? targetStructure + : normalizedSections.map((s) => s.type); + + if (!structureToUse.length && normalizedSections.length) { + return { title, sections: normalizedSections }; } - if (targetStructure && targetStructure.length) { - return { - title, - sections: targetStructure.map((t) => ({ type: t, lyrics: "" })), - }; - } + const remaining = [...normalizedSections]; + const takeMatching = (type) => { + const index = remaining.findIndex((s) => s.type === type); + if (index !== -1) { + return remaining.splice(index, 1)[0]; + } + return remaining.shift() || null; + }; - return { title, sections: [] }; + const sections = structureToUse.map((segmentType) => { + const normalizedType = normalizeStructureType(segmentType); + if (!segmentRequiresLyrics(normalizedType)) { + return { type: normalizedType, lyrics: "" }; + } + const matched = takeMatching(normalizedType); + return { + type: normalizedType, + lyrics: matched?.lyrics || "", + }; + }); + + const remainingTextual = remaining.filter((item) => + segmentRequiresLyrics(item.type) + ); + sections.push(...remainingTextual); + + return { title, sections }; }, [projectTitle, projectLyrics, projectConfig]); const [titleValue, setTitleValue] = useState(initial.title || ""); @@ -131,7 +154,11 @@ const Lyrics = ({ navigation }) => { alertMessage("Titre manquant", "Veuillez renseigner un titre."); return; } - const invalid = (sections || []).some((s) => !(s?.lyrics || "").trim()); + const invalid = (sections || []).some((s) => { + const type = normalizeStructureType(s?.type); + if (!segmentRequiresLyrics(type)) return false; + return !(s?.lyrics || "").trim(); + }); if (invalid) { alertMessage( "Champs incomplets", @@ -141,7 +168,9 @@ const Lyrics = ({ navigation }) => { } const normalizedNewLyrics = (sections || []).map((s) => ({ type: normalizeStructureType(s?.type), - lyrics: (s?.lyrics || "").trim(), + lyrics: segmentRequiresLyrics(normalizeStructureType(s?.type)) + ? (s?.lyrics || "").trim() + : "", })); const normalizedOldLyrics = Array.isArray(projectLyrics) ? projectLyrics.map((s) => ({ @@ -349,6 +378,18 @@ const Lyrics = ({ navigation }) => { ? 170 : 225; const placeholder = meta?.label || label; + const requiresLyrics = segmentRequiresLyrics(key); + if (!requiresLyrics) { + return ( + + {label} + + {getPromptLabelForStructure(key)} — section instrumentale + sans paroles. + + + ); + } return ( { return STRUCTURE_SEGMENTS[key] || null; }; +export const segmentRequiresLyrics = (type) => { + const meta = getSegmentMeta(type); + if (!meta) return true; + return meta.requiresLyrics !== false; +}; + export const OPTIONAL_STRUCTURE_SEGMENTS = Object.keys( STRUCTURE_SEGMENTS ).filter((key) => STRUCTURE_SEGMENTS[key]?.optional); @@ -121,6 +132,7 @@ export const normalizeStructureType = (value) => { export const sanitizeStructureList = (structure = []) => { if (!Array.isArray(structure)) return []; const output = []; + let shouldInjectPreChorus = false; structure.forEach((segment) => { const type = normalizeStructureType(segment); @@ -144,10 +156,47 @@ export const sanitizeStructureList = (structure = []) => { } } + if (type === "pre_chorus") { + shouldInjectPreChorus = true; + return; + } + output.push(type); }); - return output; + let result = output; + + if (shouldInjectPreChorus) { + const baseWithoutPre = result.filter((item) => item !== "pre_chorus"); + const hasRefrain = baseWithoutPre.some((item) => item === "refrain"); + const expanded = []; + + baseWithoutPre.forEach((item, idx) => { + if (item === "refrain") { + const previous = expanded[expanded.length - 1]; + if (previous !== "pre_chorus") { + expanded.push("pre_chorus"); + } + } + expanded.push(item); + }); + + if (!hasRefrain) { + expanded.push("pre_chorus"); + } + + result = expanded; + } + + const introIndex = result.findIndex( + (item) => item === "short_intro" || item === "long_intro" + ); + if (introIndex > 0) { + const [intro] = result.splice(introIndex, 1); + result.unshift(intro); + } + + return result; }; export const formatStructureLabel = (type, occurrence = 1) => {