Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
58 lines
3.1 KiB
Plaintext
58 lines
3.1 KiB
Plaintext
/* ── 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 = "";
|
|
}
|