android: the handoff — the pending rounds, the pipeline, and what is unverified
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.
This commit is contained in:
1 parent
867fd42972
commit
9f0601027c
400 files changed
+26401
-3
No files matched your search
@@ -0,0 +1,153 @@
|
||||
|
||||
/** Re-read the payload for the loaded track without emptying the panel —
|
||||
* a switch flips and the words stay on screen. The cache only, never the
|
||||
* network. */
|
||||
async function reread() {
|
||||
const id = identity();
|
||||
if (!id) return;
|
||||
const g = gen;
|
||||
try {
|
||||
const p = await api.lyricsGet({ title: id.title, artist: id.artist, album: id.album, durationSecs: id.durationSecs, path: id.path, refresh: null });
|
||||
if (g === gen) apply(p, true);
|
||||
} catch { /* what is showing stands */ }
|
||||
}
|
||||
|
||||
/** The app's automatic timing correction, on or off for this track. The
|
||||
* switch flips at once; the backend's own number comes with the re-read. */
|
||||
export async function setAutoSync(enabled) {
|
||||
const id = identity();
|
||||
if (!id) return;
|
||||
const before = lyr.autoSyncOff;
|
||||
lyr.autoSyncOff = !enabled;
|
||||
try {
|
||||
await api.lyricsSetAutoSync(id.title, id.artist, id.durationSecs, enabled);
|
||||
} catch (e) {
|
||||
lyr.autoSyncOff = before;
|
||||
player.error = String(e);
|
||||
return;
|
||||
}
|
||||
await reread();
|
||||
}
|
||||
|
||||
/* ── the vocal-reduced copy ──────────────────────────────────────────────
|
||||
The desktop's offline DSP: a band-limited centre cancel, not a learned
|
||||
separator, and the wording stays honest about that. The copy plays under
|
||||
the SAME title, artist and duration, so the lyric identity is still the
|
||||
recording's and the words keep following it. The track starts again from
|
||||
the top — there is no seeking a file the engine has only just been given. */
|
||||
async function swapPath(e, to) {
|
||||
/* a COPY of the entry's track, never the row itself: tracksOf caches one
|
||||
array per record and hands the SAME objects to the record page and to
|
||||
the queue, so writing the path in place would leave the record pointing
|
||||
at a cache file the LRU drops out from under it. The desktop does not
|
||||
rewrite its queue item either — it plays the copy as its own thing. */
|
||||
e.t = { ...e.t, path: to };
|
||||
await playQueueAt(player.qi);
|
||||
}
|
||||
export async function reduceVocals() {
|
||||
const e = current();
|
||||
const t = e?.t;
|
||||
if (!t?.path || t.stream || lyr.reducing) return false;
|
||||
const from = t.path;
|
||||
lyr.reducing = true;
|
||||
note("Reducing the vocals — this can take a moment");
|
||||
try {
|
||||
const res = await api.lyricsPrepareInstrumental(from);
|
||||
/* the render takes seconds: starting the OLD track's copy now would
|
||||
hijack whatever the queue has moved on to */
|
||||
const now = current();
|
||||
if (!res?.path || now?.t?.path !== from) {
|
||||
note("The track changed while preparing — try again");
|
||||
return false;
|
||||
}
|
||||
lyr.reduced = { from, to: res.path };
|
||||
await swapPath(now, res.path);
|
||||
return true;
|
||||
} catch (err) {
|
||||
player.error = String(err);
|
||||
return false;
|
||||
} finally {
|
||||
lyr.reducing = false;
|
||||
}
|
||||
}
|
||||
export async function backToOriginal() {
|
||||
const r = lyr.reduced;
|
||||
const e = current();
|
||||
if (!r || e?.t?.path !== r.to) return false;
|
||||
await swapPath(e, r.from);
|
||||
lyr.reduced = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── the tap-to-sync room ────────────────────────────────────────────────
|
||||
The song plays and one tap lands per line; the taps ARE the timing.
|
||||
Saving writes the track's own document (lyrics_save_lines), the top of
|
||||
the waterfall, the same shelf a paste lands on — and the backend drops
|
||||
every line without a time, so a half-tapped set is stored as the lines
|
||||
that have one. */
|
||||
export const tapSync = $state({
|
||||
rows: [], // [{ time_ms: number|null, text }]
|
||||
cursor: 0, // the row the next tap stamps
|
||||
identity: null, // the track the rows belong to, captured at open
|
||||
saving: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
const plainLines = (text) => String(text ?? "").split("\n").map((l) => l.trim())
|
||||
.filter((l) => l && !/^\[.*\]$/.test(l));
|
||||
|
||||
/** Open the room for the playing track. An already-timed set is re-tapped
|
||||
* from scratch: the room shows three lines, not a list, so it has no
|
||||
* per-line handle to nudge — it has the tap. */
|
||||
export function openTapSync() {
|
||||
const id = identity();
|
||||
if (!id) return;
|
||||
/* peeled by the back gesture mid-tap: the same track's stamps come back
|
||||
rather than the room starting over */
|
||||
if (tapSync.identity?.key === id.key && tapSync.rows.some((r) => r.time_ms != null)) { openTiming(); return; }
|
||||
const texts = (lyr.lines.length ? lyr.lines.map((l) => l.text) : plainLines(lyr.plain))
|
||||
.filter((t) => String(t || "").trim());
|
||||
if (!texts.length) return;
|
||||
tapSync.rows = texts.map((text) => ({ time_ms: null, text }));
|
||||
tapSync.cursor = 0;
|
||||
tapSync.identity = id;
|
||||
tapSync.saving = false;
|
||||
tapSync.error = "";
|
||||
openTiming();
|
||||
}
|
||||
export function closeTapSync() {
|
||||
closeTiming();
|
||||
tapSync.rows = [];
|
||||
tapSync.cursor = 0;
|
||||
tapSync.identity = null;
|
||||
tapSync.saving = false;
|
||||
tapSync.error = "";
|
||||
}
|
||||
/** What the lyrics sheet says under the row when a set was left mid-tap. */
|
||||
export function tapProgress() {
|
||||
const id = identity();
|
||||
if (!id || tapSync.identity?.key !== id.key) return null;
|
||||
const done = tapSync.rows.filter((r) => r.time_ms != null).length;
|
||||
return done ? { done, total: tapSync.rows.length } : null;
|
||||
}
|
||||
/** The taps, as this track's own timed document. */
|
||||
export async function saveTapSync() {
|
||||
const id = tapSync.identity;
|
||||
const rows = tapSync.rows.filter((r) => r.time_ms != null && r.text.trim());
|
||||
if (!id || !rows.length || tapSync.saving) return false;
|
||||
tapSync.saving = true;
|
||||
tapSync.error = "";
|
||||
try {
|
||||
const p = await api.lyricsSaveLines(id.title, id.artist, id.durationSecs,
|
||||
rows.map((r) => ({ time_ms: Math.max(0, Math.round(r.time_ms)), text: r.text })));
|
||||
/* the same track is still playing: show the result at once */
|
||||
if (identity()?.key === id.key && p && p.status) apply(p);
|
||||
closeTapSync();
|
||||
return true;
|
||||
} catch (e) {
|
||||
tapSync.error = String(e).replace(/^Error:\s*/, "").slice(0, 140);
|
||||
return false;
|
||||
} finally {
|
||||
tapSync.saving = false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user