/* ── the measured envelope, and the rough pass over it ─────────────────── The desktop's editor offers the same two things (LyricsSyncEditor.svelte): a strip showing where the recording has voice in it, and a fill that lays the lines across the ACTIVE stretches only. src/lib/lyric-sync.js is the desktop's own generator — pure, shared, unit-tested — so the phone imports it rather than keeping a second copy of the arithmetic. It refuses instead of guessing when the measurement shows nothing usable: an estimate is a starting point for the tap, never finished timing, and the room says so everywhere it shows. The envelope belongs to the track, not to the store — it is measured on open and thrown away with the room. */ async function loadActivity(id) { /* a streamed track is a cache file the engine may still be filling, and the reducer refuses one on the same decode path for the same reason */ if (!id?.path || player.track?.stream) { tapSync.reading = "none"; return; } tapSync.reading = "reading"; try { const a = await api.lyricsVocalActivity(id.path); if (tapSync.identity?.key !== id.key) return; // the room moved on tapSync.activity = a?.frames?.length ? a : null; tapSync.reading = tapSync.activity ? "ready" : "none"; } catch { if (tapSync.identity?.key === id.key) tapSync.reading = "none"; } } /** Lay every line across the stretches the envelope says are sung, then put * the cursor back at the top: the taps CORRECT the estimate from there. */ export function estimateTimings() { const a = tapSync.activity; if (!a || !tapSync.rows.length) return false; /* the room's rows ARE the lines: openTapSync built them from the document the sheet is showing. So the generator's two halves are called directly rather than roughSync's own text parse, which drops a section label a SYNCED document still carries and would hand back a shorter list that no longer lines up with the rows the room is asking about. */ const filled = distributeLines(tapSync.rows.map((r) => String(r.text ?? "").trim()), activeSegments(a.frames, a.frameMs)); /* it refuses rather than guesses when there is too little measured voice to lay the lines across — and that is the only reason it refuses, so the room can say which one it is */ if (!filled) { tapSync.error = "There is not enough voice in this song to estimate from."; return false; } for (let i = 0; i < filled.length; i++) tapSync.rows[i].time_ms = filled[i].time_ms; tapSync.cursor = 0; tapSync.estimated = true; tapSync.error = ""; return true; } /** Drop it: every line back to no time, ready for the tap. A rough fill * stamps every row at once, so Undo has nothing left to take back — this * is the way out of an estimate that is wrong. */ export function clearEstimate() { for (const r of tapSync.rows) r.time_ms = null; tapSync.cursor = 0; tapSync.estimated = false; tapSync.error = ""; }