Hopping between songs of an already-cached album still ran the full
"finding best source…" resolve for audio sitting complete on disk — a
second of popup between every click. The player now remembers the source
each stream item was queued from (localStorage, LRU-capped at 400) and,
before any search, asks the backend whether that exact file is
byte-complete in the stream cache. If it is, the track plays those bytes
immediately: no resolver, no popup, and no online requirement — cached
audio is playable offline, so the check sits before requireOnline.
The memory is a hint, never a promise. It is written eagerly when an item
is queued to PLAY, because a source that never delivers leaves no complete
cache file and the byte-complete probe rejects it later at no cost;
eviction and half-finished transfers fall back to the normal resolver the
same way. First play of a track records it, every play after that is
instant.
CatalogTrack gains artist_id — Deezer sends it on every search row and it was
simply unparsed — and rank, its 0-1,000,000 popularity figure. Together they
let a listener count be resolved for the PERFORMING artist of each row rather
than for the query's top artist, so a "panda bear" search shows Daft Punk's
"Doin' it Right (feat. Panda Bear)" at its real 464,046.
Resolution rides the existing deezer_linked_mbid -> lastfm_listener_ranks chain
behind a new catalog_track_listeners command, deliberately kept separate from
catalog_search: that one is typed against per keystroke and must never wait on
MusicBrainz's 1 req/s gate.
catalog_artist_profile backs the hero's middle third — MusicBrainz tags off the
cached artist document plus one Last.fm artist.getinfo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The track preview modal could hold a machine at 25% GPU. Three separate
causes, each measured in isolation with a kill-style ladder and, once the
GPU-engine counter proved to be clock-relative (a lighter workload read
HIGHER), with clock-immune process CPU-seconds.
Idle drain. The scrim's backdrop-filter is re-executed on every presented
frame -- a backdrop filter is never cached -- so its cost is presents/sec
times window area. Two infinite animations kept the modal presenting at the
display's refresh rate forever: the eq capsule's bars animated `height` (a
layout property, so style/layout/paint on the main thread every vsync) and
the cover glow's halo-breathe kept a blur(48px) copy recompositing. Either
one alone kept the pipeline hot, which is why killing one changed nothing.
The glow is deleted outright at Kyle's request; the eq bars now scale with
a stepped transform whose three delays are multiples of the step interval,
so all three jump on the same instants and one present serves them all.
Playing steady state: 7.2-10.5% of a core down to 1.4-2.2%. Paused: 0.2%.
Scrolling. The animations turned out to be innocent here -- reverting them
moved the number not at all. The cost was per-step work: re-decoding the
full artwork for the background wash, a main-thread accent scan, a new
cover mount, three times a second. The wash is now a 96x58 canvas with the
blur/saturate/brightness baked in at draw time and cached per artwork, and
an overlay preloader walks the manifest once at open -- nearest to the
active card first, capped at 60 -- filling the decoded-image, wash and
accent caches ahead of the user. A rapid-nav jump-cut was tried and
rejected on sight; the animations stay full at every rate.
Backend. preview_resolve swept the clip directory on every call, and any
landing outside the +/-1 prefetch window paid a full Deezer mint, download,
validate, write and loudness measure. The fetch-store-measure core is
factored out and a new preview_prewarm walks the session's track ids at
open, paced 250ms on top of the existing Deezer throttle and starting 1.5s
in so the first clip owns the network, halted by a generation guard on
dispose. A burst landing on a never-visited track is now audible in 154ms
off local disk. The sweep is throttled to ten minutes, and dispose deletes
prewarmed files instead of orphaning them until the hourly reap.
The debug overlay's GPU row was summing the counters machine-wide, so it
read roughly double the app's share with Discord and DWM folded in. It now
reports `app X% - sys Y%`, the app figure restricted to our process tree,
matching Task Manager's per-process rows.
There is exactly one Deezer artist called "Wand" (1213178) and it holds the
LA psych band, a Brazilian funk MC, an Indonesian act, an Indian one, a 2020
house project and whoever released Hard Knox in 2009 — 46 of its 72 rows are
somebody else's. Ranking search results cannot help (there is one candidate)
and neither can credit role (every row is primary-credited to it).
The Apple namesake filter that should have caught this switched itself off:
APPLE_EXACT_ARTIST_CANDIDATE_LIMIT is 8 and Apple has TEN artists named Wand,
so the candidate fetch failed closed and every catalogue came back empty. It
fails closed for a good reason at page-load time — it picks the winner by
overlap with the requested artist's own Deezer releases, which on a fused
record are partly the co-tenants'. Circular evidence has to fail closed.
So the answer is not to raise that cap but to stop asking the fused list.
artist_release_ownership runs in the DEFERRED identity pass, where the linked
MBID has already named the artist, and picks the Apple performer that
reproduces THAT catalogue. Non-circular, so 20 candidates is safe. Evidence is
MB release groups of every type, the verified Apple catalogue, and the label
sets of both (MB at one sighting, Apple at two — Apple files two rafftheraff
singles under the LA band). Pruning only runs where a row is provably carried
by a different same-named Apple artist, so the gate opens on 4 of the 55
artists measured and stays shut on Wednesday's twenty namesakes, Jungle's
sixteen and Roosevelt's fourteen.
The same evidence repairs the opposite failure. The guest guard keeps an
appears_on row only if MB credits it to the artist's own MBID, which assumes
MB mirrors collaborations. It does for Watch the Throne, whose artist CREDIT
names both. It does not when the collaboration is its own entity: VULTURES 1,
VULTURES 2 and KIDS SEE GHOSTS appear nowhere in Ye's 315 release groups, and
all three were being erased from his page. Apple's artistName credit, read as
a list of principals, restores them without restoring guest spots — Estelle's
"American Boy (feat. Kanye West)" is still hers.
A fused record's PORTRAIT is wrong for the same reason its releases are: it is
whichever co-tenant uploaded last, a real photograph of the wrong person, so
deezerPortraitMissing cannot see it. The verdict is persisted, because the
search dropdown is typed against per keystroke and can never afford the
lookups that produced it. Its store is deliberately not in net-cache-v1 —
prune_disk_cache deletes every json there by mtime, and a verdict is not a
cached response.
Measured live: Wand 72 -> 26, Hot Toddy 36 -> 16, Crazy P 68 -> 51,
Turnover 30 -> 25, Kanye 50 -> 64; Weezer, Pond, Billy Cobb, JPEGMAFIA and
Daft Punk untouched. The page's "wand." wordmark went with the co-tenants and
should have: it was traced from the Indonesian single's cover.
Preview modal audio, five rounds:
- True track-change crossfade: scheduleSettle no longer silences the
outgoing clip at t=0; preview_load's equal-power fade owns both ramps,
gated on the incoming voice's first Playing event (audible_marks), with
failure/overstay fallbacks. CROSSFADE_MS = 340 — locked to the reel's
card-slide so audio and visuals land together.
- Two-neighbour lease prefetch + a 15-min on-disk clip byte-cache
(recent_clips): steps in BOTH directions crossfade in ~15 ms; revisits
never re-download.
- Adaptive settle (isolated steps fire immediately, bursts coalesce) and
anticipatory auto-advance (step at clip end minus 400 ms) so auto-play
crossfades over the tail instead of gapping.
- Session resume memory: revisited tracks continue where they left off
(decoder-level start offset via play_local_from/resume_at); finished
tracks restart from the top.
- Open/close crossfades with the MAIN player: fade-pause with the
desired-paused latch landing at ramp end, fade-resume from the paused
position; MAIN_FADE_GEN guards; focus handshake relaxation documented
in focusBridge.
Volume normalization (Settings > Playback, default on):
- Dependency-free BS.1770/EBU R128 meter in the player crate (K-weighting,
400 ms gated blocks, incremental) + analyzer that reads the same growing
file playback streams from — first plays are measured at download speed.
- Gain layered under user volume; audible start GATED on a preliminary
measurement (10 s of audio or 800 ms budget) so the level is right from
the first sample and never changes mid-track; prelim boosts capped 3 dB,
peak-aware clipping guard, persistent loudness-cache.json.
- Gapless boundaries get their own sample-accurate gain via
SetNextNormGainDb, fed by preload analysis.
Debug overlay (Ctrl+Shift+D): process-tree CPU / PDH GPU / working-set +
private-commit RAM / network rates / FPS / current-track download progress
read from the backend's own StreamProgress / live normalization state /
preview session phase. Polls only while visible.
131 JS + player + app tests green; every path verified live over CDP.
Rust (2A):
- metadata: deezer_track_uncached (fresh mint, cache-updating, paced) and
fetch_preview_media — dedicated client whose custom redirect policy
re-validates every hop against the media allowlist; bounded body read
(cap enforced on received bytes, not Content-Length's promise); Date
header captured for provider-clock lease math. httpdate dep added.
- src-tauri/preview.rs: second Player instance + soulstream:preview event
channel; preview_resolve (fresh mint → host gate → MIME gate → ID3v2
shell strip → MPEG sync check → clip file + opaque lease id → safe_for_ms
mirroring lease.js); preview_load with start_paused via the
desired-paused latch; preview_control/dispose; player_pause_acked
exposing the Phase 0.4 ack on the MAIN player. 9 unit tests.
JS (2B):
- api.js: preview command surface (opaque lease ids only).
- nativeTransport.js: stamps {sessionId, generation, sessionKey} onto
backend events; cleared context makes stragglers dispatch nothing.
- productionResolver.js: DTO → MediaLease, failure classes preserved.
- focusBridge.js: production coordinator binding with the documented
renderer-side generation invariant (buffering counter + gateway).
- service.js: the orchestrator — settle window, resolve→load→play under
focus, single capped retry, one forward lease prefetch with safe-window
expiry, stale re-checks before every transport command, close/handoff.
The service constructs its transport around its own dispatch — the
factory signature exists because a pre-built transport silently loses
the event wiring (caught by test).
70 JS tests passing (npm test); metadata 46 + app preview 9 in cargo.
The previous .git directory was found gutted (only info/ remained), so
history is unrecoverable locally. This commit restores version control
with the tree exactly as it stands: the working app, the preview-tracks
launcher on the artist hero, the scrollable-track-previews design bundle
(docs + approved mockups), and the Phase 0 validation artifacts.