From efef23753b3e920e2ab6b05adf833dabef2b9d6a Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 17 Jun 2026 05:12:19 -0400 Subject: [PATCH] docs(phase-12): spec waveform-visualizer generalization + NowPlayingHero rewire 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. --- PLAN.md | 73 +++ ...e-12-waveform-visualizer-generalization.md | 448 ++++++++++++++++++ 2 files changed, 521 insertions(+) create mode 100644 product-notes/phase-12-waveform-visualizer-generalization.md diff --git a/PLAN.md b/PLAN.md index 2e4fa8a..61f2f38 100644 --- a/PLAN.md +++ b/PLAN.md @@ -254,6 +254,79 @@ Sequenced as **eight waves**; the critical path is `11.A → 11.B → 11.C → 1 --- +## Phase 12 — Waveform Visualizer Generalization + NowPlayingHero Rewire + +Take the landed Mix waveform visualizer (the WebGL2 lava renderer + its seven-knob controls, Phase 10 +reframe) and **make it the one release-cardinal visualizer** — serving Mix detail, all Release Detail +pages, *and* the home-page NowPlaying card — instead of a Mix-only backdrop forked three ways. **Two +deliverables, one engine, DRY/SOLID the explicit ask.** Full design, the extraction analysis, the datum +decision, wave decomposition, and open questions: `product-notes/phase-12-waveform-visualizer-generalization.md`. + +**Central finding (verified read, 2026-06-17): the engine is already release-cardinal below the surface.** +`MixWaveformVisualizer`'s bridge keys on `ReleaseEntryKey` + `TrackId` (not Mix); the renderer is a pure +function of a loudness datum + duration; the controls/state are renderer-agnostic. The *only* genuinely +Mix-coupled surface is (1) the datum **fetch** (`GET api/release/{entryKey}/mix/waveform` 404s unless +`Medium == Mix`) and (2) the high-res datum **source** (the `mix-waveforms` vault, Mix-only). Everything +else is just *named* `Mix*`. So "generalize from Mix to all releases" is a **rename + a data-source +generalization, not a rebuild** — the renderer, bridge, controls, read-only contract all carry forward +from the Phase 10 reframe unchanged. + +**Crucial data fact (verified): non-Mix releases are not a data gap.** *Every* uploaded track already gets +a **512-bucket** waveform profile (`UnifiedTrackService.UploadAsync` → `waveform-profiles` vault, the +datum the player-bar `WaveformSeeker` consumes). Mixes *additionally* get a duration-derived **high-res** +datum (~333 samples/sec, `mix-waveforms` vault, CMS-triggered). So the only question for non-Mix is +*resolution*, not existence — and §8a frames that as the one real product call (recommend: serve the +existing 512-bucket profile for ambient non-Mix backdrops in v1; high-res-for-all is a roadmap upgrade). + +**Deliverable 2 — NowPlayingHero overhaul.** `NowPlayingCard.razor` today animates **20 hardcoded +CSS-bounce bars** with no audio coupling (the "stochastic" visualizer). Replace them with the *same* +`WaveformVisualizer`, mounted inside the existing player cascade and pointed at "whatever is playing right +now" — so the home card shows the **real** waveform of the live track, Mix or not. The payoff of the +generalization: the NowPlaying card is *just another host* of the one engine, not a fork. The one genuine +engineering wrinkle is that the renderer assumes full-viewport (`position: fixed; inset: 0`, +clip-to-footer) and the card needs it container-relative — recommend a `Fill` mode parameter (spec §6c). + +**Design discipline.** Rename the engine to its abstraction (`MixWaveformVisualizer` → `WaveformVisualizer`, +etc.) — a `Mix`-named component on a Cut page is a lie that cements the wrong model. Variance rides +**composition** (a new optional `Backdrop` slot on `ReleaseDetailScaffold`; per-host control suppression), +never a `switch (medium)` in the engine (memory *One source, multiple views*; scaffold's "variance rides a +slot, never a flag" idiom, Phase 9 §5.3). The lava controls stay a **Mix affordance by default**; +Cut/Session (if they get a backdrop) mount it controls-suppressed as ambient — the seam supports controls- +everywhere later with no engine change (memory *Design for adaptability up front*). + +Sequenced as **four waves**: `12.A → 12.B → (12.C ‖ 12.D)`. + +- **12.A — Rename to the abstraction (mechanical, no behavior change).** `Mix*` → `Waveform*` across the + five C#/Razor files + the TS module + its import path + the DI registration. **Load-bearing + prerequisite** — every later wave references the generalized names. Acceptance: Mix detail identical; + diff is identifiers only. +- **12.B — Generalize the datum fetch + endpoint.** New unauthenticated `GET + api/release/{entryKey}/waveform` resolving the **best-available** datum (high-res for Mix-with-datum; + 512-bucket per-track profile otherwise); `IReleaseDataService.GetReleaseWaveform`; bridge calls it + instead of `GetMixWaveform`. **Depends on 12.A.** Direction A (recommended) needs no decision to start; + the high-res-for-all alternative (Direction B) does — §8a. +- **12.C — `Backdrop` slot on `ReleaseDetailScaffold` + mount on detail pages.** Promote the full-bleed / + foreground-stacking / dynamic-footer-clip pattern into the scaffold as an optional `Backdrop` slot; Mix + re-expresses its current mount through it; Cut mounts the controls-suppressed ambient backdrop; Session + mounts directly (it doesn't compose the scaffold — spec §3e) **if** §8b opts non-Mix in. **Depends on + 12.B.** +- **12.D — NowPlayingHero rewire.** Replace the synthetic bars with a contained, controls-suppressed + `` driven by the live cascaded player; add the `Fill`/container-sizing mode (spec + §6c). **Depends on 12.A + 12.B; independent of 12.C** (different host). Acceptance: home card shows the + real playing-track waveform, at-rest when nothing plays; no synthetic bars remain. + +**Open product decisions (spec §8, need Daniel before the dependent wave):** (a) **non-Mix datum +resolution** — existing 512-bucket per-track profile (Direction A, recommended, zero new compute) vs. +high-res-for-all (Direction B, new CMS/API/content + backfill); gates 12.B's *richness*, blocks nothing if +A. (b) **Do Cut/Session get a backdrop, and with controls?** — recommend ambient-on-all-media / +controls-Mix-only; note even Mix-only still wants 12.A/B/D for the NowPlaying win. (c) **What is a +multi-track Cut's waveform?** — recommend first track by `TrackNumber` for v1; only bites if (b) gives Cut +a backdrop. (d) **NowPlaying container-sizing + home-page perf** — staff-engineer-owned (`Fill` mode; +`isPlaying`-gated rAF means an idle home page pays nothing), flagged so a lava lamp on the landing page is +no surprise. + +--- + ## Working with this file - **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing. diff --git a/product-notes/phase-12-waveform-visualizer-generalization.md b/product-notes/phase-12-waveform-visualizer-generalization.md new file mode 100644 index 0000000..9fb919b --- /dev/null +++ b/product-notes/phase-12-waveform-visualizer-generalization.md @@ -0,0 +1,448 @@ +# 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**: + +1. **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. +2. **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 on `ReleaseEntryKey` + `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 `Backdrop` slot on + `ReleaseDetailScaffold` (§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 `IsActivePlayer` + gating, the `isPlaying`-gated rAF loop — all preserved. Extend the fetch, not the contract. +- **No new control model.** The seven knobs and `…ControlState` stay 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 `MixWaveformVisualizer` on other +> pages." Resist it: a `MixWaveformVisualizer` mounted 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 `` 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 `` to `Backdrop` and keeps its + `TopRightAction` lava-lamp + `TopContent` knob band — same as today, just expressed through the slot. +- **Session / Cut** *may* supply the same `` to `Backdrop` with 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 `WaveformVisualizerControlState` still 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` + a `BackdropControls` + slot 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 the `Backdrop` slot 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 `` 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 `TrackId` via `IsActivePlayer`. +- 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 `
`** 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 **``** scoped to the card, driven by +the **live cascaded player**: + +- The NowPlaying card already cascades `IStreamingPlayerService` (it reads `Player.CurrentTrack`). +- Mount `` — the same bridge, the same playback coupling. Because the card is + *inside* the cascade, `IsActivePlayer` is 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 the `waveform-placeholder`. +- The datum comes from §5's `GetReleaseWaveform` keyed 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 + `ReleaseEntryKey` change (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)** — a `Fill` mode 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 = 2` + cap and the `isPlaying`-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}/waveform` resolving best-available datum (Direction A, §5); `IReleaseDataService. + GetReleaseWaveform`; bridge calls it instead of `GetMixWaveform`. **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 — `Backdrop` slot on the scaffold + mount on detail pages.** Promote the full-bleed/foreground/ + footer-clip pattern into `ReleaseDetailScaffold` as an optional `Backdrop` slot (§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 + `` driven by the live player (§6); add the `Fill`/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) + +1. **Rename clean.** The engine is named for its abstraction (`WaveformVisualizer*`); the Mix detail page + is visually and behaviorally identical to before the rename. +2. **Release-cardinal datum.** `GET api/release/{entryKey}/waveform` returns a datum for *any* release + that has one (high-res for Mix-with-datum, 512-bucket per-track otherwise), 404 → graceful blank. +3. **Mix unchanged.** The Mix detail page still renders the high-res lava with the seven-knob bar, at + parity with the Phase 10 reframe. +4. **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. +5. **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. +6. **One engine.** Mix detail, the (opted-in) Cut/Session backdrop, and the NowPlaying card all consume + the *same* `WaveformVisualizer` component + renderer + state — verified by there being exactly one of + each, no per-host fork. +7. **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. +8. **Bridge intact.** Single-owner handle, idempotent datum guard, `IsActivePlayer` gating, and the + `isPlaying`-gated rAF loop are unchanged across all mounts; track-change in the NowPlaying card + re-fetches the datum cleanly.