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.
154 lines
6.2 KiB
Plaintext
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;
|
|
}
|
|
}
|