android: the room can measure the song, and a cold artist page stops waiting on one call
The tap-to-sync room draws the recording's own vocal band (lyrics_vocal_activity, the desktop's coarse envelope) as a strip under the head, and one button lays every line across the stretches it says are sung — the desktop's own generator, shared and unit-tested, which refuses rather than guesses when there is too little voice to read. The result is labelled an estimate everywhere it shows, the cursor goes back to the top so the taps correct it, and one more tap drops the whole thing back to an empty set, because a rough fill stamps every line at once and leaves Undo nothing to take back. The artist page now fires the hero seed and the overview beside the full page, the way the desktop stages them: a hero Kyle picked on this phone comes back off the file with no network at all, the discography paints from the overview's first page until the exhaustive pass lands with the classified one, and both lanes are dropped with the page when a hero changes. ANDROID.md's "Not done yet" is rewritten as the true map of the port — what is absent by design and why, what is waiting on Rust rather than on a file picker, and one bullet about the artwork viewer that had been done for rounds.
This commit is contained in:
1 parent
5738c2d7bf
commit
44eda8dff5
9 files changed
+374
-39
No files matched your search
+54
-4
@@ -497,21 +497,39 @@ const NEWS = [
|
||||
{ title: "Infinity Repeating gets a video", url: "https://example.com/news/2", source: "NME", date: Math.floor(Date.now() / 1000) - 86400 * 2 },
|
||||
];
|
||||
const RELATED = [["Justice", 411], ["Air", 412], ["Kavinsky", 413], ["Chemical Brothers", 414]].map(([name, id], i) => ({ id, name, picture: i % 2 ? null : svgArt(40 + id, name[0]), album_count: 3, fans: 900000 - id * 100 }));
|
||||
/* An artist whose exhaustive page the mock deliberately keeps for four
|
||||
seconds. Everything on screen before then came from the two fast lanes —
|
||||
which is the only way the walk can tell that they ran. The titles are
|
||||
deliberately plain, so the classifier's title heuristics leave them alone
|
||||
and the tabs are exactly what the counts below say. */
|
||||
const CAT_SLOW = { id: 415, name: "Halcyon Drift", picture: null, album_count: 7, fans: 48000 };
|
||||
const SLOW_ALBUMS = [
|
||||
{ id: 4150, title: "Halcyon Drift", release_date: "2014-04-08", record_type: "album", track_count: 11 },
|
||||
{ id: 4151, title: "Second Light", release_date: "2016-09-23", record_type: "album", track_count: 10 },
|
||||
{ id: 4152, title: "Slow Ascent", release_date: "2019-02-15", record_type: "album", track_count: 12 },
|
||||
{ id: 4153, title: "First Signal", release_date: "2013-06-01", record_type: "single", track_count: 2 },
|
||||
{ id: 4154, title: "Fourth Wall", release_date: "2021-05-07", record_type: "album", track_count: 9 },
|
||||
{ id: 4155, title: "Long Winter", release_date: "2023-01-20", record_type: "album", track_count: 10 },
|
||||
{ id: 4156, title: "Open Water", release_date: "2024-08-30", record_type: "album", track_count: 11 },
|
||||
].map((a) => ({ ...a, artist: CAT_SLOW.name, cover: svgArt((a.id * 17) % 360, a.title[0]), edition_group: null, is_soundtrack: false }));
|
||||
/* the heroes chosen this session — pick_image → set_artist_hero */
|
||||
const heroes = {};
|
||||
let follows = [];
|
||||
function mockCatalog(cmd, args) {
|
||||
if (cmd === "catalog_artist") {
|
||||
const id = Number(args?.artistId);
|
||||
const a = id === CAT_ARTIST.id ? CAT_ARTIST : RELATED.find((r) => r.id === id);
|
||||
const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id);
|
||||
if (!a) return Promise.reject(new Error("catalog_artist: unknown artist"));
|
||||
const own = a === CAT_ARTIST;
|
||||
/* the exhaustive page, kept four seconds on purpose: whatever is on
|
||||
screen before then came from the fast lanes and nowhere else */
|
||||
const slow = a === CAT_SLOW;
|
||||
return new Promise((r) => setTimeout(() => r({
|
||||
artist: a, banner: heroes[a.id] ?? null, banners: heroes[a.id] ? [heroes[a.id]] : [], banner_custom: !!heroes[a.id], logos: [],
|
||||
popular_tracks: own ? CAT_TRACKS.slice(0, 7).map((t, i) => ({ ...t, listeners: 2_400_000 - i * 310_000 })) : [],
|
||||
albums: own ? CAT_DISCO : [{ id: 900 + id, title: a.name + " Live", artist: a.name, cover: null, release_date: "2015-01-01", record_type: "album", track_count: 9, edition_group: null, is_soundtrack: false }],
|
||||
albums: own ? CAT_DISCO : slow ? SLOW_ALBUMS : [{ id: 900 + id, title: a.name + " Live", artist: a.name, cover: null, release_date: "2015-01-01", record_type: "album", track_count: 9, edition_group: null, is_soundtrack: false }],
|
||||
top_genre: "Electronic", socials: [],
|
||||
}), 220));
|
||||
}), slow ? 4000 : 220));
|
||||
}
|
||||
if (cmd === "catalog_artist_socials") {
|
||||
const id = Number(args?.artistId);
|
||||
@@ -527,7 +545,7 @@ function mockCatalog(cmd, args) {
|
||||
page gives none (the related ones), the only Popular there is */
|
||||
if (cmd === "catalog_artist_popular") {
|
||||
const id = Number(args?.artistId);
|
||||
const a = id === CAT_ARTIST.id ? CAT_ARTIST : RELATED.find((r) => r.id === id);
|
||||
const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id);
|
||||
if (!a) return Promise.resolve([]);
|
||||
const rows = id === CAT_ARTIST.id
|
||||
? CAT_TRACKS.slice(0, 7)
|
||||
@@ -551,6 +569,9 @@ function mockCatalog(cmd, args) {
|
||||
if (cmd === "catalog_search") {
|
||||
const q = String(args?.query || "").toLowerCase();
|
||||
const hit = /daft|discovery|one more|punk/.test(q);
|
||||
/* the slow artist answers the identity search at once — it is only
|
||||
the exhaustive page that is kept waiting */
|
||||
if (/halcyon/.test(q)) return new Promise((r) => setTimeout(() => r({ artists: [CAT_SLOW], albums: [], tracks: [] }), 120));
|
||||
return new Promise((r) => setTimeout(() => r(hit
|
||||
? { artists: [CAT_ARTIST, CAT_NAMESAKE], albums: CAT_ALBUMS, tracks: CAT_TRACKS.slice(0, 8) }
|
||||
: { artists: [], albums: [], tracks: [] }), 120));
|
||||
@@ -587,6 +608,26 @@ function mockCatalog(cmd, args) {
|
||||
if (cmd === "library_art_prewarm") return Promise.resolve();
|
||||
if (cmd === "refresh_all_album_art") return new Promise((r) => setTimeout(() => r(140), 300));
|
||||
if (cmd === "refresh_artist_art") return new Promise((r) => setTimeout(() => r(26), 300));
|
||||
/* the banner ahead of the page. A hero picked on this phone is answered
|
||||
from the override with no network at all, exactly as the Rust does —
|
||||
and the field really is snake_case there (no rename_all on it) */
|
||||
if (cmd === "catalog_artist_hero_seed") {
|
||||
const id = Number(args?.artistId);
|
||||
if (heroes[id]) return Promise.resolve({ banner: heroes[id], banner_custom: true });
|
||||
const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id);
|
||||
if (!a) return Promise.resolve(null);
|
||||
return new Promise((r) => setTimeout(() => r({ banner: svgArt(210, a.name[0]), banner_custom: false }), 200));
|
||||
}
|
||||
/* the one-round content stage: the raw first page of releases, before the
|
||||
exhaustive pass classifies them. It goes through checkedArtistPayload,
|
||||
so `artist.id` must equal the argument or the wrapper throws */
|
||||
if (cmd === "catalog_artist_overview") {
|
||||
const id = Number(args?.artistId);
|
||||
const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id);
|
||||
if (!a) return Promise.reject(new Error("catalog_artist_overview: unknown artist"));
|
||||
const albums = id === CAT_SLOW.id ? SLOW_ALBUMS.slice(0, 4) : id === CAT_ARTIST.id ? CAT_DISCO.slice(0, 4) : [];
|
||||
return new Promise((r) => setTimeout(() => r({ artist: a, popular_tracks: [], albums }), 300));
|
||||
}
|
||||
if (cmd === "catalog_art") return Promise.resolve(svgArt(200, "D"));
|
||||
if (cmd === "stream_cache_ready") return Promise.resolve(cacheReady.has(args?.path) || (xfer && xfer.path === args?.path && xfer.received >= xfer.total));
|
||||
if (cmd === "preload_stream") {
|
||||
@@ -832,6 +873,15 @@ function mockNet(cmd, args) {
|
||||
}
|
||||
/* the vocal-reduced copy: a render takes a beat, and it is a new file */
|
||||
if (cmd === "lyrics_prepare_instrumental") return new Promise((r) => setTimeout(() => r({ path: String(args?.path || "") + ".instrumental.wav", cached: false, durationSecs: 214 }), 400));
|
||||
/* 214 s at a frame per 100 ms: three sung stretches with quiet between
|
||||
them, which is exactly what activeSegments is built to find — 150 s of
|
||||
active time, far over its 4 s floor, so the rough pass lays all three
|
||||
lines and the first one lands at 10 s */
|
||||
if (cmd === "lyrics_vocal_activity") {
|
||||
const sung = [[10, 60], [80, 140], [160, 200]];
|
||||
const frames = Array.from({ length: 2140 }, (_, i) => (sung.some(([a, b]) => i >= a * 10 && i < b * 10) ? 0.9 : 0.04));
|
||||
return new Promise((r) => setTimeout(() => r({ frameMs: 100, frames }), 250));
|
||||
}
|
||||
if (cmd === "lyrics_clear") return Promise.resolve();
|
||||
if (cmd === "clear_play_history") return Promise.resolve();
|
||||
if (cmd === "refresh_album_art") return new Promise((r) => setTimeout(() => r("C:\\mock\\cover.jpg"), 200));
|
||||
|
||||
Reference in new issue
Block a user