Generalize the Mix-only WebGL lava visualizer into one release-cardinal WaveformVisualizer serving Mix detail, all Release Detail pages, and the home NowPlaying card. Four waves; flags the non-Mix datum-resolution call.
31 KiB
Phase 12 — Waveform Visualizer Generalization + NowPlayingHero Rewire (Design Spec)
Status: design-complete, implementation-ready (one open product decision flagged in §8 — the non-Mix datum-resolution call — and two smaller calls). Author: product-designer. Date: 2026-06-17. No code has been written by this doc.
This phase has two deliverables that share one engine:
- Generalize the landed Mix waveform visualizer (the WebGL2 lava renderer + its seven-knob controls) from a Mix-only backdrop into a release-cardinal visualizer that every Release Detail page can host — Cuts, Sessions, and Mixes alike.
- Overhaul the home-page
NowPlayingHero(NowPlayingCard) so its "Now Playing" animation is the real waveform visualizer driven by live playback, replacing the 20 hardcoded CSS-animated bars (the "stochastic bullshit").
The explicit ask is DRY / SOLID: one reusable visualizer engine serving Mix detail, all Release Detail pages, and the NowPlayingHero — not three forks. This spec's central finding makes that cheap: the engine is already release-cardinal below the surface. The work is extraction and a data-source generalization, not a rebuild.
Cross-references (read before implementing):
product-notes/phase-10-mix-visualizer-lava-reframe.md— the lava renderer this generalizes. The CPU-physics wax-blob model, the OKLab three-color gradient, the seven-knob control model, the bridge contract, and the read-only contract all carry forward unchanged. This spec does not re-derive any of them; it changes where the engine lives, what feeds it, and who hosts it.product-notes/mix-visualizer-webgl-renderer.md— the renderer architecture (pipeline, datum-as-texture, bridge, rAF loop). The §2 contract carries forward; §4/§7 are already superseded by the reframe.DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor[.cs/.css]— the Blazor bridge. Already keyed onReleaseEntryKey+TrackId, not on Mix. Renamed and re-pointed by this phase (§3, §4).DeepDrftPublic.Client/Controls/MixVisualizerControls.razor[.cs/.css],DeepDrftPublic.Client/Services/MixVisualizerControlState.cs,DeepDrftPublic.Client/Controls/MixZoomMapping.cs— the controls + state + mapping. Renamed, otherwise unchanged.DeepDrftPublic/Interop/visualizer/MixVisualizer.ts— the WebGL2 renderer module. Renamed; the only logic change is how the datum's time-mapping is established when no high-res mix datum exists (§5).DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor— the shared detail chrome. The visualizer becomes a scaffold-level concern via a new optional backdrop slot (§3c).DeepDrftPublic.Client/Pages/MixDetail.razor,SessionDetail.razor,CutDetail.razor— the three release-detail hosts.DeepDrftPublic.Client/Controls/NowPlayingCard.razor[.css],NowPlaying.razor— the home-page now-playing card carrying the stochastic bars (§6).DeepDrftAPI/Controllers/ReleaseController.cs,TrackController.cs— the waveform endpoints; the data-source question lives here (§5, §8).DeepDrftContent/Processors/WaveformProfileService.cs,MixWaveformResolution.cs,Constants/VaultConstants.cs— the (content-agnostic) compute/store path and the two vaults.
1. The central finding — the engine is already release-cardinal
Before any plan: a read of the live code shows the "Mix coupling" is mostly nominal, not structural.
The visualizer is named Mix* throughout, but its architecture is release-generic:
| Layer | Reality on disk today | Mix-coupled? |
|---|---|---|
MixWaveformVisualizer bridge inputs |
ReleaseEntryKey (string) + TrackId (long?) + cascaded player |
No — already release-cardinal |
| Playback coupling | IsActivePlayer gates on TrackId matching the cascaded player's current track |
No — works for any release's track |
Renderer (MixVisualizer.ts) |
datum texture + scroll/zoom geometry + wax-blob physics + OKLab gradient | No — pure function of a loudness datum + duration |
| Controls + state | seven normalized dials, scoped persistence, Changed seam |
No — renderer-agnostic |
| Datum fetch | IReleaseDataService.GetMixWaveform(entryKey) → GET api/release/{entryKey}/mix/waveform |
Yes — 404s unless Medium == Mix |
| Datum source | the high-res mix-waveforms vault, keyed by MixMetadata.WaveformEntryKey |
Yes — only Mixes get the high-res datum |
| Names / comments | Mix* everywhere |
cosmetic |
So the genuinely Mix-specific surface is exactly two things: the fetch endpoint that gates on
Medium == Mix, and the high-res datum that only Mixes have. Everything else is a rename.
This is the SOLID seam the whole phase turns on. The renderer and bridge already obey the right abstraction ("render a loudness datum coupled to a playing track"); they were just named for their first consumer. Generalizing is: rename to the abstraction, give the abstraction a datum source that exists for every release, and let three hosts mount it. No new renderer, no fork.
The crucial data fact (verified): every uploaded track already has a waveform profile.
UnifiedTrackService.UploadAsync calls WaveformProfileService.ComputeAndStoreAsync(...) at upload time
for every track, storing a 512-bucket profile in the waveform-profiles vault keyed by the
track's EntryKey (this is the datum the player-bar WaveformSeeker already consumes). Mixes
additionally get a duration-derived high-res datum (~333 samples/sec) in the separate
mix-waveforms vault, triggered by a CMS action. So non-Mix releases are not a data gap — they have
a low-res datum today. The only question is whether 512 buckets is enough resolution for the lava
visualizer on a Cut/Session, or whether they should get the high-res treatment too (§5, §8 — the one real
product decision).
2. Goal and scope boundary
Goal. One reusable WaveformVisualizer (renamed from MixWaveformVisualizer) + its lava renderer +
its controls, mounted as a backdrop on all three Release Detail pages and on the home-page NowPlaying
card, fed by a release-cardinal datum source that exists for every release. The lava controls stay a
Mix affordance unless Daniel says otherwise (§3d). The NowPlaying card drives the same engine off live
playback (§6).
In scope.
- Rename + relocate the visualizer engine to a release-cardinal identity (
MixWaveformVisualizer→WaveformVisualizer,MixVisualizerControls→WaveformVisualizerControls,MixVisualizerControlState→WaveformVisualizerControlState,MixVisualizer.ts→WaveformVisualizer.ts,MixZoomMapping→WaveformZoomMapping). Pure renames; no behavior change. (§3a) - Generalize the datum source. A release-cardinal fetch that returns the best available datum for any release — the high-res mix datum when present, the per-track 512-bucket profile otherwise (§5). This is the load-bearing data change.
- Host the visualizer on every Release Detail page via a new optional
Backdropslot onReleaseDetailScaffold(§3c), so Cut/Session/Mix mount it without each page re-implementing the full-bleed wrapper. - Rewire the NowPlayingHero to mount the visualizer driven by the live cascaded player, replacing the 20 hardcoded CSS bars (§6).
- Decide where the lava controls live per medium — Mix keeps the seven-knob bar; Cut/Session default to a controls-less ambient backdrop (§3d), revisitable.
Out of scope / unchanged.
- No renderer rewrite. The wax-blob physics, the OKLab gradient, the collision model, the seven dials — all carry forward from the Phase 10 reframe exactly. This phase moves and renames the engine and changes its input plumbing, never its art.
- No bridge redesign. The single-owner bridge, the idempotent datum guard, the
IsActivePlayergating, theisPlaying-gated rAF loop — all preserved. Extend the fetch, not the contract. - No new control model. The seven knobs and
…ControlStatestay as-is (renamed). No new dials. - No CMS change unless §5 lands the "high-res for all media" option (then a generalized waveform trigger touches the CMS — flagged, not committed).
- No playback-control change. Read-only contract holds everywhere, including the NowPlaying card — the home card visualizes, it does not become a transport.
3. Generalizing the visualizer onto every Release Detail page
3a. The rename (Wave 12.A — pure, mechanical, no behavior change)
Rename the engine to its abstraction. This is a mechanical sweep with zero behavior change, done first so every later wave references the generalized names:
| Today (Mix-named) | Generalized |
|---|---|
MixWaveformVisualizer.razor[.cs/.css] |
WaveformVisualizer.razor[.cs/.css] |
MixVisualizerControls.razor[.cs/.css] |
WaveformVisualizerControls.razor[.cs/.css] |
MixVisualizerControlState.cs (+ DI registration) |
WaveformVisualizerControlState.cs |
MixZoomMapping.cs |
WaveformZoomMapping.cs |
MixVisualizer.ts (+ the ./js/visualizer/MixVisualizer.js import path) |
WaveformVisualizer.ts |
DDIcons.LavaLamp / LavaLampFilled |
keep (the lava-lamp glyph is the controls affordance, still Mix-only by default — §3d) |
The ReleaseEntryKey / TrackId parameters and the fetch keep working unchanged through the rename.
The mix-waveforms vault name and MixMetadata.WaveformEntryKey stay (they are still where the high-res
mix datum lives — generalizing the fetch doesn't require renaming the mix-specific high-res store;
see §5). Acceptance: the Mix detail page looks and behaves identically after the rename; the only diff
is identifiers.
DRY note. The temptation is to skip the rename and "just reuse
MixWaveformVisualizeron other pages." Resist it: aMixWaveformVisualizermounted on a Cut page is a lie that every future reader has to decode, and it cements the wrong mental model right when we're trying to break it. The rename is cheap and it is the SOLID move — name the thing for its abstraction, not its first caller.
3b. What stays specialized (the abstraction boundary)
Generalizing does not mean flattening every medium to the same look. The clean Liskov boundary:
- Shared (the engine): the renderer, the bridge, the controls component, the state, the datum contract, the playback coupling, the read-only contract. One copy, consumed by all hosts.
- Per-host (the composition): whether the visualizer is mounted, whether the lava controls are
exposed, and what datum the host points it at. These ride host composition (slots + parameters),
never a
switch (medium)inside the engine. A medium that wants no visualizer mounts none; a medium that wants the ambient backdrop but no knobs mounts the backdrop with controls suppressed.
This is the same "variance rides a slot, never a flag" discipline the scaffold already uses for
Header/Hero/TopRightAction (Phase 9 §5.3) — extended to the backdrop.
3c. The hosting seam — a Backdrop slot on ReleaseDetailScaffold (Wave 12.C)
Today the Mix page mounts the visualizer outside the scaffold (a sibling <MixWaveformVisualizer> then
a .mix-detail-foreground wrapper, with the scaffold inside MudContainer). Session mounts nothing.
Cut mounts nothing. To let every medium host the visualizer DRY-ly:
Add an optional RenderFragment? Backdrop slot to ReleaseDetailScaffold, rendered as the
full-bleed position: fixed; inset: 0 layer behind the scaffold's content (the scaffold's existing
container becomes the foreground stacking context — promote the mix-detail-foreground pattern into the
scaffold so it is the default, not a Mix bespoke). A host that supplies no Backdrop gets today's plain
background (Liskov: absent slot = no backdrop, no regression).
- Mix supplies
<WaveformVisualizer ReleaseEntryKey=… TrackId=… />toBackdropand keeps itsTopRightActionlava-lamp +TopContentknob band — same as today, just expressed through the slot. - Session / Cut may supply the same
<WaveformVisualizer>toBackdropwith controls suppressed (§3d) — an ambient living backdrop behind the hero. Whether they do is a product call (§8b).
Why the scaffold, not each page. The full-bleed wrapper, the foreground stacking context, and the
footer-clip plumbing (the dynamic-footer overflow clip from the reframe §2c) are all chrome, and the
scaffold is where chrome lives. Putting the backdrop on the scaffold means the clip logic, the stacking
context, and the mount point are written once. SessionDetail is the lone holdout that doesn't compose
the scaffold today — see §3e.
3d. Where do the lava controls live per medium? (the controls boundary)
The seven-knob lava bar is an expert tuning surface whose identity is "the lava lamp." Two clean positions, and a recommendation:
- Recommended default: lava controls are a Mix affordance only. Mix keeps the lava-lamp toggle + the
seven-knob bar. Cut/Session, if they mount the backdrop, mount it controls-suppressed — an ambient
living gradient/lava field behind the hero with no knobs, no lava-lamp button. Rationale: the controls
are a deliberate "I want to tune the lava" gesture that fits the Mix's full-bleed-visualizer-is-the-point
page; on a Cut album page or a Session hero page the visualizer is ambience, not the subject, and a
knob bar there competes with the content. The shared
WaveformVisualizerControlStatestill supplies the default dial values, so Cut/Session backdrops render with Daniel's tuned defaults — they just can't be changed in place. - Alternative (if Daniel wants parity): controls everywhere. The
Backdrop+ aBackdropControlsslot pair lets any medium opt into the knob bar. Cheap to add later precisely because the controls are already a separate component over shared state — this is a composition decision, not an engine change. Designing theBackdropslot now leaves the door open (memory: design the seam, defer the feature).
This is open question §8b. Default to Mix-only controls unless Daniel says otherwise; the seam supports either without an engine change.
3e. The SessionDetail scaffold question
SessionDetail deliberately does not compose ReleaseDetailScaffold (it wraps its own
MudContainer + ReleaseHeroOverlay). If Session is to host the backdrop via the scaffold's new slot,
either (a) SessionDetail adopts the scaffold (a larger refactor, out of scope here — Session's
divergence was a deliberate Phase 11 call), or (b) Session mounts <WaveformVisualizer> directly with
its own full-bleed wrapper (small, local, mirrors what Mix does inline today). Recommend (b) if
Session gets a backdrop at all — don't reopen the Session-vs-scaffold decision for this. Cut does
compose the scaffold, so Cut gets the backdrop for free via the slot. This asymmetry is fine: the slot
serves scaffold-composing media; the one non-composing page mounts the shared engine directly. The
engine is still single-source either way — only the mount differs, which is exactly the per-host
variance §3b sanctions.
4. The bridge, generalized (Wave 12.B)
The bridge (WaveformVisualizer.razor.cs, ex-MixWaveformVisualizer) needs one real change beyond
the rename: its datum fetch must resolve a datum for any release, not only Mixes.
Today: await ReleaseData.GetMixWaveform(ReleaseEntryKey) → 404 for non-Mix → blank backdrop.
Generalized: await ReleaseData.GetReleaseWaveform(ReleaseEntryKey) → returns the best available
datum for the release (§5 decides what "best available" means and where the resolution happens). The
bridge stays otherwise identical:
- Still keys the fetch on
ReleaseEntryKey, fetch-once-per-key guard intact. - Still derives duration from the cascaded player (
PlayerDurationSeconds) — note: the duration source is the player, which works for any release's playing track, so the time↔sample mapping generalizes for free. - Still gates playback coupling on
TrackIdviaIsActivePlayer. - Still pushes the seven control dials, the datum, playback, and theme through the unchanged handle.
The PlaybackPosition composability fallback (the no-player-cascade path, used by the NowPlaying card if
it ever runs outside the cascade — though it won't, §6) stays as the documented escape hatch.
The single open data question (§5, §8a): does GetReleaseWaveform return the low-res 512-bucket
per-track profile for non-Mix releases (cheap, already exists, slightly coarse for the lava), or do we
extend the high-res compute to all media (richer, but new CMS/compute work)?
5. The datum source — the one real data decision
The visualizer renders a loudness datum + a duration. Two datums exist in the system today:
| Datum | Vault | Resolution | Who has it | Keyed by |
|---|---|---|---|---|
| Per-track profile | waveform-profiles |
512 buckets (fixed) | every track (computed at upload) | track EntryKey |
| Mix high-res datum | mix-waveforms |
~333 samples/sec (duration-derived, up to ~2M) | Mixes only (CMS-triggered) | MixMetadata.WaveformEntryKey (= the mix track's EntryKey) |
So non-Mix releases are not a data gap — they have the 512-bucket profile. The question is purely resolution. Three directions, materially different in cost and shape:
Direction A — "best available, no new compute" (recommended for v1).
GetReleaseWaveform returns the high-res mix datum when the release is a Mix with a stored datum;
otherwise it falls back to the release's single-track 512-bucket profile (resolve the track via the
release → its EntryKey → waveform-profiles). For multi-track Cuts, use the first track's profile (or
a chosen representative — §8c). Cost: one new release-cardinal endpoint + a service method that picks
the source; zero new compute, zero CMS work, ships immediately. Trade-off: a Cut/Session backdrop
renders at 512 buckets — fine for an ambient backdrop (the lava doesn't need quarter-note resolution
when it's behind a hero and not the subject), arguably coarse if a Cut ever wants the full-bleed Mix
treatment. Since §3d makes Cut/Session ambient by default, 512 buckets is enough for the v1 look.
Direction B — "high-res for all media."
Generalize TriggerMixWaveformAsync into a release-cardinal TriggerReleaseWaveform that computes the
duration-derived high-res datum for any single-track release (Mix and Session both being single-track),
storing into a generalized waveform vault. Cost: a generalized compute path + a CMS generate action
exposed for non-Mix media + a backfill for existing releases. Larger, touches CMS + API + content.
Trade-off: uniform high quality everywhere, but multi-track Cuts still need a per-track or a
concatenated-album answer (the "what is an album's waveform" question, §8c), which Direction B doesn't by
itself resolve. Defer unless the ambient 512-bucket look proves too coarse on screen.
Direction C — "low-res is fine everywhere, drop the high-res special-case."
Use the 512-bucket per-track profile for everyone including Mix, retiring the mix-waveforms high-res
path. Rejected: the high-res mix datum exists precisely because the Mix visualizer's max-zoom window
(one quarter note at 180 BPM) under-samples badly at 512 buckets on a long mix (MixWaveformResolution
rationale). Throwing it away regresses the Mix — the exact page this engine was built for. Don't.
Recommendation: ship Direction A. It is DRY (one endpoint, one fallback rule), ships with zero new compute, and is sufficient given §3d's ambient-backdrop framing for non-Mix media. Keep Direction B on the roadmap as the upgrade if/when a Cut wants the full Mix treatment. This is open question §8a — Daniel's call on whether 512-bucket ambient is acceptable for non-Mix, or he wants high-res-for-all from the start.
Endpoint shape (Direction A). A new unauthenticated GET api/release/{entryKey}/waveform that
resolves: Mix-with-datum → mix high-res; else → first/representative track's 512-bucket profile; else →
404 (blank backdrop, graceful). This supersedes the bridge's call to the Mix-gated
/mix/waveform for the general case; the /mix/waveform route can stay (the new endpoint can delegate to
the same mix-vault read internally) or be folded in — staff-engineer's call. IReleaseDataService gains
GetReleaseWaveform(entryKey); the bridge calls it.
6. Overhauling the NowPlayingHero (Wave 12.D)
6a. What's there now
NowPlayingCard.razor shows the now-playing title/sub plus, when Player.IsLoaded, a waveform-bars
div of 20 hardcoded <div class="waveform-bar"> elements, each with fixed --h-lo/--h-hi/--dur CSS
custom properties driving a CSS keyframe bounce. It is purely synthetic — no audio data, no coupling
to the actual signal; it bounces on a fixed loop whenever anything is loaded. This is the "stochastic
bullshit."
6b. The rewire — mount the real engine, driven by live playback
Replace the waveform-bars block with a mounted <WaveformVisualizer> scoped to the card, driven by
the live cascaded player:
- The NowPlaying card already cascades
IStreamingPlayerService(it readsPlayer.CurrentTrack). - Mount
<WaveformVisualizer ReleaseEntryKey="@Player.CurrentTrack.Release?.EntryKey" TrackId="@Player.CurrentTrack.Id" />— the same bridge, the same playback coupling. Because the card is inside the cascade,IsActivePlayeris true for whatever is actually playing, and the visualizer scrolls/animates to the real signal. When nothing is playing, it sits at its at-rest slice (the bridge already handles the no-active-player state → static), which replaces thewaveform-placeholder. - The datum comes from §5's
GetReleaseWaveformkeyed on the current track's release — so the home card shows the real waveform of whatever track the listener started, Mix or not.
This is the payoff of the generalization: the NowPlaying card is just another host of the same engine, pointed at "whatever is playing right now" instead of "this page's release." No NowPlaying-specific renderer, no fork — the DRY win the brief demands.
6c. Constraints specific to the NowPlaying context
- Live, not static. Unlike a Release Detail page (where the visualizer's release is fixed to the page),
the NowPlaying card's release changes as the track changes. The bridge already re-fetches on
ReleaseEntryKeychange (the fetch-once-per-key guard re-arms when the key changes), so track-change → new datum is handled. Verify the guard re-fetches cleanly on key change (it keys on_loadedReleaseKey == ReleaseEntryKey, so a new key re-fetches — correct). - Small surface, controls-less. The card is a small hero panel, not a full-bleed page. Mount the
visualizer controls-suppressed (no lava-lamp, no knob bar — same ambient framing as §3d for
Cut/Session) and sized to the card, not
position: fixed. Flagged for staff-engineer (§8d): the renderer's footer-clip + full-viewport assumptions (position: fixed; inset: 0, clip-to-footer) are written for a full-page backdrop; mounting it in a contained card needs the canvas to size to its container instead of the viewport. This is a real renderer-hosting wrinkle — the engine assumes full-window today. Either (i) parameterize the visualizer's sizing (full-viewport vs. fill-container), or (ii) the NowPlaying card is the forcing function to make the canvas container-relative. Recommend (i) — aFillmode parameter — because it also future-proofs any contained mount (an embed, a CMS preview). This is the one genuine engineering subtlety in the NowPlaying rewire; everything else is composition. - Performance. A WebGL2 lava render on the home page (the highest-traffic, first-paint surface) is a
heavier ask than on a detail page the user navigated to deliberately. Keep the existing
MAX_DPR = 2cap and theisPlaying-gated rAF loop (it burns no frames when nothing plays — so an idle home page pays nothing). If the lava is too heavy for the home card specifically, the controls-suppressed ambient backdrop can run a cheaper preset (fewer blobs) via a density default — but do not fork the renderer; use the existing density dial. Flagged, not committed (§8d).
6d. What the card keeps
The title/sub text block (np-title/np-sub) and the "Now Playing" label stay — only the synthetic
waveform-bars / waveform-placeholder block is replaced by the mounted visualizer. The pulsing
circle-deco rings in NowPlaying.razor are unrelated decor; leave them.
7. Wave decomposition + dependency shape
Sequenced so the mechanical rename de-risks everything, the data generalization unblocks the new hosts, and the NowPlaying rewire (the trickiest, per §6c) comes last on a proven engine.
- 12.A — Rename to the abstraction (mechanical, no behavior change).
Mix*→Waveform*across the five files + the TS module + the import path + the DI registration (§3a). Load-bearing prerequisite — every later wave references the generalized names. Acceptance: Mix detail behaves identically; diff is identifiers only. - 12.B — Generalize the datum fetch + endpoint (the data change). New
GET api/release/{entryKey}/waveformresolving best-available datum (Direction A, §5);IReleaseDataService. GetReleaseWaveform; bridge calls it instead ofGetMixWaveform. Depends on 12.A (renamed bridge). Gated by §8a (Daniel's resolution call — but Direction A needs no decision to start; B/C would). Acceptance: Mix still renders high-res; a non-Mix release now returns a (512-bucket) datum. - 12.C —
Backdropslot on the scaffold + mount on detail pages. Promote the full-bleed/foreground/ footer-clip pattern intoReleaseDetailScaffoldas an optionalBackdropslot (§3c); Mix re-expresses its current mount through the slot; Cut mounts the controls-suppressed ambient backdrop; Session mounts directly (§3e) if §8b says non-Mix gets a backdrop. Depends on 12.B (a datum to render). Acceptance: Mix unchanged; Cut/Session (if opted in) show an ambient living backdrop at their tuned defaults, no knobs. - 12.D — NowPlayingHero rewire. Replace the synthetic bars with a contained, controls-suppressed
<WaveformVisualizer>driven by the live player (§6); add theFill/container-sizing mode (§6c). Depends on 12.A + 12.B (renamed engine + a datum for whatever's playing). Independent of 12.C (different host; doesn't need the scaffold slot). Acceptance: the home card shows the real waveform of the playing track and sits at-rest when nothing plays; no synthetic bars remain.
Dependency shape: 12.A → 12.B → (12.C ‖ 12.D). 12.A is the cheap mechanical unblock; 12.B is the
load-bearing data generalization; 12.C (detail-page hosts) and 12.D (NowPlaying host) are independent
siblings off 12.B. The cold-start item is 12.A — do it first, it touches everything and risks nothing.
8. Open product decisions (need Daniel before the dependent wave)
§8a — Non-Mix datum resolution (gates 12.B's richness; blocks nothing if Direction A).
Does GetReleaseWaveform serve non-Mix releases the existing 512-bucket per-track profile (Direction
A — recommended, zero new compute, sufficient for ambient backdrops), or do we extend high-res compute
to all media (Direction B — richer, new CMS/API/content work + backfill)? Recommendation: A for v1,
B on the roadmap. Direction A can start immediately; only B/C need a decision before 12.B.
§8b — Do Cut/Session get a backdrop at all, and with controls? Three positions: (1) Mix-only — only Mix hosts the visualizer; Cut/Session stay plain (smallest, the generalization then serves Mix + NowPlaying only). (2) Ambient on all media, controls Mix-only (recommended) — Cut/Session get the living backdrop at tuned defaults, no knobs. (3) Full parity — every medium gets the backdrop and the knob bar. Recommendation: (2). Note that even (1) still wants 12.A+12.B+12.D for the NowPlaying rewire — the generalization pays for itself via the home card regardless.
§8c — What is a multi-track Cut's waveform?
A Cut album has many tracks; the visualizer renders one datum. First track? A representative/longest
track? A concatenated album-length datum (Direction B territory)? Recommendation: first track by
TrackNumber for v1 (cheap, deterministic), revisit if it reads wrong. Only bites if §8b chooses a Cut
backdrop.
§8d — NowPlaying container-sizing + home-page performance (engineering subtleties, staff-engineer-owned
but flag for Daniel). The renderer assumes full-viewport (position: fixed; inset: 0, clip-to-footer);
the NowPlaying card needs it container-relative (§6c) — recommend a Fill mode parameter. And a WebGL2
lava render on the home page's first paint is heavier than on a detail page — the isPlaying-gated rAF
means an idle home page pays nothing, but a cheaper blob-density preset for the card is a fallback if
needed. Neither blocks; both are tuning/hosting calls surfaced so Daniel isn't surprised by a lava lamp on
the landing page.
9. Acceptance criteria (observable)
- Rename clean. The engine is named for its abstraction (
WaveformVisualizer*); the Mix detail page is visually and behaviorally identical to before the rename. - Release-cardinal datum.
GET api/release/{entryKey}/waveformreturns a datum for any release that has one (high-res for Mix-with-datum, 512-bucket per-track otherwise), 404 → graceful blank. - Mix unchanged. The Mix detail page still renders the high-res lava with the seven-knob bar, at parity with the Phase 10 reframe.
- Non-Mix backdrop (if §8b opts in). A Cut and/or Session detail page shows an ambient living waveform backdrop at the tuned default dials, controls-suppressed, no regression to the hero/content.
- NowPlaying is real. The home NowPlaying card shows the actual waveform of the playing track (scrolls/animates to the real signal, changes with track changes), and sits at-rest when nothing plays. No hardcoded synthetic bars remain.
- One engine. Mix detail, the (opted-in) Cut/Session backdrop, and the NowPlaying card all consume
the same
WaveformVisualizercomponent + renderer + state — verified by there being exactly one of each, no per-host fork. - Read-only everywhere. No host (including the NowPlaying card) exposes a seek/scrub/transport via the visualizer; the read-only contract holds on every mount.
- Bridge intact. Single-owner handle, idempotent datum guard,
IsActivePlayergating, and theisPlaying-gated rAF loop are unchanged across all mounts; track-change in the NowPlaying card re-fetches the datum cleanly.