Files
SoulStream/handoff/rounds/ins-r23-lyrics-store.txt
T
KeelrLeek 9f0601027c 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.
2026-09-14 07:57:41 -05:00

154 lines
6.2 KiB
Plaintext

/** 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;
}
}