diff --git a/PLAN.md b/PLAN.md index 61f2f38..ddab404 100644 --- a/PLAN.md +++ b/PLAN.md @@ -202,22 +202,7 @@ Full design, renderer architecture, the four effects, acceptance criteria, and p ### Phase 10 — Reframe (Lava): Waves R1–R4 -A **major reframe of the Mix visualizer's effects, controls, and color model**, folded **under this same Phase 10** as a reframe wave-set (**Waves R1–R4** — labelled to stay unambiguous against the landed Waves 1–4 above). It builds on the landed Phase 10 renderer infrastructure (pipeline, datum texture, playhead interp, bridge, widened body, lava-lamp trigger) but **replaces what it paints**. Daniel tested the Phase 10 effects end-to-end and rejected the visual result: the lava read as "giant disconnected circles," the colors drifted to cyan (an HSL saturation-boost artifact), and the waveform and lava read as two unrelated things sharing a canvas. The diagnosis (staff-engineer research pass) is that the rejected look is **structural to the effect approach, not a tuning miss**. - -**This supersedes the original Phase 10 (Waves 1–4) effects/controls/color design** — `product-notes/mix-visualizer-webgl-renderer.md` §4 (effects) and §7 (popover-controls) are marked superseded with a pointer to the reframe spec. The renderer *infrastructure* carries forward unchanged. - -**The three reframes:** -- **Lava → CPU-physics wax blobs.** Keep the single-pass WebGL2 fragment renderer; add a small CPU-side per-frame physics step modeling ~16–32 Lagrangian "wax blobs" (position/velocity/temperature/radius) uploaded as uniforms and blended with `smin` SDF metaballs. The waveform and lava share **the same plane WITH real 2D elastic collision** (blob↔waveform-boundary + blob↔blob) — the waveform pushes the fluid out of its way (read-only authority preserved; the fluid never deforms the waveform). **Refined by Daniel's Wave R2 eval:** the fluid must read **flat** (an evenly-lit unified body, NOT blobs with bright pointed centers / cone-like radial gradients) and **melt into one fluid** (low viscosity / strong coalescence — "behave more like a fluid," not stiff globs in contact; the slow wax-like *motion* damping and the freely-coalescing *surface* are independent). **Energy-coupled dynamics:** higher heat/energy → **smaller bubbles + more turbulence** (many small lively turbulent bubbles at high heat; fewer, larger, calmer wax at low heat) — the lamp should feel dynamic and fun. At heat 0 the wax rests on the floor and only collision moves it (collision always on, independent of heat); at heat max many small turbulent bubbles rise/morph per second. **Tuning anchor: Daniel's sweet spot is ~20% gravity / ~100% heat** — calibrate defaults/ranges so that combination lands lively. **Rejected: a full ping-pong FBO Navier-Stokes fluid sim** — a lava lamp is high-viscosity/low-turbulence, the opposite regime; large rewrite for unwanted realism. Deliberate later upgrade only. -- **Color → three-color OKLab gradient with three motions.** One source of truth (`DeepDrftPalettes`), no hardcoded hexes. Always A→B linear from the center line outward. Three combined motions: (1) anchors A/B **rotate among three theme colors X/Y/Z** at the rotation-speed control's rate — **OKLab interpolation, never through the rainbow** (the cyan fix is structural, not a tuning dial); (2) per-bar sinusoidal variation **baked at segment entry and fixed as the segment scrolls** — realized by **keying the sinusoid to mix-time** so it travels with the segment by construction (decided 2026-06-16; the explicit ring-buffer alternative is rejected for maintainability); (3) per-bar gradient curve shifts with scroll height (mostly A at bottom → mostly B at top). The static noise/frost texture is **removed** (Daniel: makes the screen look dirty). -- **Controls → seven knobs in an inline collapse/expand knob-bar.** Replaces the four: (1) waveform scroll speed [replaces resolution/zoom as a standalone control], (2) gradient rotation speed, (3) lava gravity, (4) lava heat, (5) blob density/size, (6) collision strength (soft mush→hard up-and-out throw), (7) **waveform width** [added in the Wave R2 eval — narrows the waveform band so the lava fluid has more room to move on loud/busy songs; scales the waveform's amplitude→width mapping and its collision boundary]. **NOT a popover or drawer — an in-flow controls container BETWEEN the back link (left) and the lava-lamp toggle (right)** on the detail top row (redesigned 2026-06-16, superseding the first realization). The first implementation read "inline collapse/expand" as an `position:absolute` bar floating under the lamp (`.mix-visualizer-controls-anchor`) — it clipped to a vertical sliver and read as a detached popover; **Daniel rejected it.** The redesign: `ReleaseDetailScaffold` gains a new optional **`TopRowCenter`** slot so the top row is `back | center-controls | lamp` (Track/Cut/Session supply neither → back-link-alone, reusability preserved); the seven `RadialKnob`s live in that center slot and **expand/collapse in the layout flow** (CSS width/opacity transition, container grows horizontally between back and lamp — **no `position:absolute`, no float, no overlay, no clipping, never overlaps the masthead/hero**). On narrow widths the container wraps in-flow to a second line / drops to the scaffold's `TopContent` band — never edge-clips. The lava-lamp icon button toggles a bound bool — no MudPopover/MudDrawer. Styled to **match the NowPlaying hero aesthetic** (the session-detail hero overlay — translucent dark glass, overlay-label typography, `Color.Secondary`). Also: overflow-clip the visualizer to the **dynamic footer height** (the player bar changes height minimized/expanded) so visuals stop cleanly above it; the clip line is also the lava rest line. - -**Plus: redraw the lava-lamp glyph.** The current `DDIcons.LavaLamp` is rejected (Daniel: "form is shit, colors are shit"). Redraw to the classic 1970s silhouette — a wide truncated-cone metal **base**, a bulbous→roundedly-pointed teardrop **glass body**, a small cone **cap** ("offset cones") — with **navy fluid + moss blobs** (the theme's blue+green, faithful to the reference and on-theme) and a neutral/metallic base+cap. Authored in `DeepDrftShared.Client/Common/DDIcons.cs` as inner SVG markup (no `` wrapper; 24×24 viewBox); body silhouette `currentColor`, the two accent fills are commented literals traced to their `DeepDrftPalettes` source (SVG cannot resolve `var()`). - -Heat→intensity and collision soft↔hard transfer functions are **staff-engineer tuning tasks** (endpoints fixed in the spec — heat 0 = wax rests on floor / heat max = many small turbulent rising bubbles; collision soft = gentle mush / hard = high-elasticity up-and-out throw, wide range, smooth/no jitter — formulas not). Full design, the wax-blob model, the collision model, the three-motion color model, the inline knob-bar, the icon redraw, observable acceptance criteria, and phasing: `product-notes/phase-10-mix-visualizer-lava-reframe.md`. - -**Open / undecided (spec §10):** (a) **pause behavior** — whether the lava keeps convecting while audio is *paused* (lamp "always on") vs. the current freeze-on-pause (rAF gated on `isPlaying`) is an **undecided Daniel call**; default to freeze-on-pause until decided, switching is a localized gating change, not a model change. (b) **Future enhancement (deferred, NOT in R1–R4 scope):** a per-control **"auto-modulate" checkbox** that slowly oscillates each knob's value via a low-frequency oscillator so the visualizer drifts on its own — Daniel flagged it as a cool future idea; the knob-bar layout leaves room but nothing builds it now. - -**Sequenced as four reframe waves.** `Wave R1 → Wave R2 → (Wave R3 ‖ Wave R4)`. Wave R1 (de-noise + dynamic footer clip + icon redraw) is a cheap unblock for a clean substrate. Wave R2 (wax-blob physics + 2D collision) is the load-bearing prerequisite — prove the lava before the color and the UI. Wave R3 (OKLab three-color gradient, the three motions) and Wave R4 (seven controls — including the new waveform-width knob — + NowPlaying-styled inline knob-bar + widened state to seven properties + extended bridge handle) both depend on Wave R2 but are independent of each other. **Both prior open Daniel calls are now decided:** controls-UI is an inline collapse/expand knob-bar (not popover/drawer); per-segment color is mix-time-keyed. +**Landed:** 2026-06-17 on dev. See `COMPLETED.md` for the full completion record. --- @@ -256,74 +241,120 @@ 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`. +Take the landed Mix waveform visualizer (the WebGL2 lava renderer + its eight-knob controls, Phase 10 +reframe) and **make it the one track-cardinal visualizer** — serving Mix detail, all Release Detail +pages, *and* the home-page NowPlaying card — rendering the waveform of **whatever track is currently +playing/selected**, instead of a Mix-only treatment forked three ways. **Two deliverables, one engine in +three hosting modes, DRY/SOLID the explicit ask.** Full design, the extraction analysis, the per-track +model, Direction B compute, 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.** +**Keystone model correction (Daniel, 2026-06-17): the datum is PER-TRACK, not per-release.** *"Each track +in the release must get the metadata… the release is just the host."* Every track carries its own high-res +waveform datum; the visualizer renders the *currently playing/selected* track's datum, and the release is +merely the host surface. This *simplifies* the design — it aligns with the bridge already keying on +`TrackId`, and it **dissolves** the old "what is a multi-track Cut's waveform?" question (no release-level +datum to choose). Threaded through the datum source, the endpoint shape, the bridge, and acceptance. + +**Central finding (verified read, 2026-06-17): the engine is already track-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. +Mix-coupled surface is (1) the datum **fetch** (per-release, `GET api/release/{entryKey}/mix/waveform` 404s +unless `Medium == Mix`) and (2) the high-res datum **source** (the `mix-waveforms` vault, Mix-track-only). +Everything else is just *named* `Mix*`. So "generalize from Mix to all tracks" is a **rename + a per-track +high-res compute 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). +**Datum decision (Daniel, 2026-06-17): Direction B — high-res for ALL media.** Today every uploaded track +gets a **512-bucket** profile (`UnifiedTrackService.UploadAsync` → `waveform-profiles` vault, consumed by +the player-bar `WaveformSeeker`); only **Mix tracks** *additionally* get the duration-derived **high-res** +datum (~333 samples/sec, `mix-waveforms` vault, CMS-triggered). Direction B **generalizes the high-res +compute to every track**: the content compute path goes medium-neutral, the upload path computes a per-track +high-res datum for every new track, the CMS generate action generalizes off Mix-only, and a **backfill** +populates existing tracks. The cheaper road (serve the existing 512-bucket profile to non-Mix, zero new +compute — old "Direction A") is **declined** in favor of uniform high-res. So 12.B is no longer "a new +endpoint" — it is a content + upload + CMS + backfill + fetch slice (split into 12.B1 / 12.B2 below). -**Deliverable 2 — NowPlayingHero overhaul.** `NowPlayingCard.razor` today animates **20 hardcoded +**Three hosting modes of the one engine (Daniel corrected "backdrop").** *"backdrop?? MIXES doesn't really +have a backdrop?"* — right: on Mix the visualizer is the full-bleed **centerpiece that IS the page**, not +something behind content. The one engine is hosted three ways (spec §3f): **mode A — visualizer-is-the-page** +(Mix detail, full-bleed, seven-knob bar); **mode B — ambient environment** (Cut/Session detail, living +texture *behind* the hero+content — this is the only mode that is genuinely a "backdrop"); **mode C — +contained live element** (NowPlaying card, a bounded live readout, `Fill`-sized to the card). Same engine, +same datum contract — variance is entirely in hosting composition. **Controls (Daniel, full parity, §8b):** +the seven-knob bar + lava-lamp toggle ride **every Release Detail host** — Mix, Cut, **and** Session (modes +A and B both carry the controls, not Mix-only); only the contained NowPlaying card (mode C) suppresses them +by default (one open sub-question — §8b-followup). + +**Deliverable 2 — NowPlayingHero overhaul (mode C).** `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). +`WaveformVisualizer`, mounted inside the existing player cascade and pointed at the **current track** — so +the home card shows the **real** high-res waveform of the live track, Mix or not. The payoff of the +generalization: the NowPlaying card is *just another host* of the one engine. 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*). +**composition** (a new optional `Ambient` slot on `ReleaseDetailScaffold` for mode B; Mix keeps its own +mode-A mount; the card is a mode-C contained mount; 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 slot is named `Ambient` not `Backdrop` precisely because Mix doesn't use it. The lava +controls ride **every Release Detail host** (Mix, Cut, Session — full parity, Daniel's §8b call); only the +contained NowPlaying card mounts controls-suppressed by default. The seam still supports flipping the card +to controls-on later with no engine change (memory *Design for adaptability up front*). -Sequenced as **four waves**: `12.A → 12.B → (12.C ‖ 12.D)`. +Sequenced as **five waves**: `12.A → 12.B2 → (12.C ‖ 12.D)`, with **12.B1 a parallel server-side track** +(`12.B1 → 12.B2`) that can start cold day one. - **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. +- **12.B1 — Generalize the high-res compute to every track + backfill (Direction B, the data change).** + Generalize the duration-derived compute off Mix-only (`WaveformProfileService` / `MixWaveformResolution`), + store per-track keyed by `EntryKey` in a (renamed) `track-waveforms` vault, add per-track high-res compute + to `UnifiedTrackService.UploadAsync`, generalize the CMS generate action to any track, and run the + **Daniel-gated backfill** for existing tracks (§8a-new). **Independent of 12.A** (server/content-side). + The new load-bearing heavy. Acceptance: every track has a high-res datum; new uploads get one; the + generate action works for any track. +- **12.B2 — Per-track datum fetch + bridge rewire.** New track-cardinal `GET + api/track/{trackEntryKey}/waveform` (spec §5b); `GetTrackWaveform`; bridge resolves the *current track's* + `EntryKey` and re-fetches on **track** change (not release change). **Depends on 12.A + 12.B1.** + Acceptance: Mix renders the same high-res lava via the track-cardinal fetch; a non-Mix track returns + high-res. +- **12.C — `Ambient` slot on `ReleaseDetailScaffold` + mount on detail pages (mode B, full parity).** + Promote the full-bleed / foreground-stacking / dynamic-footer-clip pattern into the scaffold as an optional + `Ambient` slot; Cut mounts the ambient layer **with the full seven-knob bar + lava-lamp toggle** (full + parity); Session mounts directly **also full-parity** (it doesn't compose the scaffold — spec §3e). Mix is + **unchanged** (mode A keeps its own mount + controls). Also makes the state-scoping call (recommend one + shared `WaveformVisualizerControlState`). **Depends on 12.B2.** **§8b resolved (full parity) — no longer + gated**; Cut and Session ship with both the ambient layer and the controls. +- **12.D — NowPlayingHero rewire (mode C).** Replace the synthetic bars with a contained, + controls-suppressed `` driven by the live cascaded player, pointed at the current + track; add the `Fill`/container-sizing mode (spec §6c). **Depends on 12.A + 12.B2; independent of 12.C** + (different host). Acceptance: home card shows the real playing-track high-res 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. +**Resolved by Daniel (2026-06-17), kept visible per file convention:** datum resolution → **Direction B** +(high-res all media; 512-bucket-fallback "Direction A" declined); multi-track-Cut datum → **dissolved by +the per-track model** (renders the current track's datum, no album-representative choice); Cut/Session +hosting + controls → **full parity (option 3)**: all three hosting modes ship **and** the seven-knob bar + +lava-lamp toggle ride every Release Detail host (Mix, Cut, Session), not Mix-only — the three-mode *layout* +framing is retained, the change is that controls are no longer Mix-suppressed on Cut/Session (the old "mode +1 Mix-only" and "controls Mix-only" alternatives are both closed). **Newly open (created by the full-parity +flip + Direction B + per-track):** (a) **§8b-followup — do full-parity controls extend onto the NowPlaying +home card (mode C)?** Full parity was answered against the *detail* pages; the small contained home card +stays **controls-suppressed by default** (a seven-knob bar on a compact now-playing card may be awkward) — +recommend keeping it suppressed, but flag for Daniel; one-line composition flip in 12.D either way. +(b) **§8a-new — backfill shape + gate**: one-shot migration/script vs. a CMS +batch action over the generalized generate action (recommend the CMS action; Daniel-gated to *run* either +way; the fetch 404s gracefully for not-yet-backfilled tracks so it can ship before the backfill completes). +(c) **§8b-new — per-track high-res compute cost** (flag only): upload latency (recommend inline; deferral is +the escape hatch) + storage growth (every track now stores a high-res datum, a multi-track Cut stores N — +modest, surfaced not blocking). (d) **§8d — 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. --- diff --git a/product-notes/phase-12-waveform-visualizer-generalization.md b/product-notes/phase-12-waveform-visualizer-generalization.md index 9fb919b..4e39e09 100644 --- a/product-notes/phase-12-waveform-visualizer-generalization.md +++ b/product-notes/phase-12-waveform-visualizer-generalization.md @@ -1,22 +1,39 @@ # 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.** +Status: **design-complete, implementation-ready.** Daniel resolved the three §8 open questions on +2026-06-17 — committing **Direction B** (high-res compute for all media), correcting the datum model to +**per-track, not per-release**, and resolving §8b to **full parity on the lava controls**: the visualizer +rides every Release Detail host (Mix, Cut, Session) **and the seven-knob bar + lava-lamp toggle ride all +three** — not Mix-only. The three-hosting-mode *layout* framing (visualizer-is-the-page on Mix / ambient +environment on Cut/Session / contained on NowPlaying) is retained; the change is that controls are now +present on all three detail-page hosts, not suppressed on Cut/Session. The spec below is revised to those +three. One small sub-question remains open (§8b-followup): whether full-parity controls also extend onto +the small NowPlaying home card (default: **no** — the card stays controls-suppressed). 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. + from a Mix-only treatment into a **track-cardinal visualizer** that every Release Detail page can host + — Cuts, Sessions, and Mixes alike — rendering the waveform of **whatever track is currently + playing/selected** on that page. 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. +cheap: the engine is *already* track-cardinal below the surface. The work is extraction, a per-track +high-res compute generalization (Direction B), and three hosting modes of the one engine — not a rebuild. + +**The keystone model correction (Daniel, 2026-06-17).** The waveform datum is **per-track, not +per-release.** *"Each track in the release must get the metadata. The waveform visualizer metadata should +be tied to the track, right? The release is just the host."* Every track gets its own high-res waveform +datum; the release is merely the *host* surface, and the visualizer renders **the datum of whatever track +is currently playing/selected**. This *simplifies* the design — it aligns with the bridge already keying on +`TrackId`, and it **dissolves** the old "what is a multi-track Cut's waveform?" question (there is no +release-level datum to choose; the visualizer just shows the current track's). Threaded through §4, §5, the +endpoint shape, and the acceptance criteria below. Cross-references (read before implementing): @@ -35,15 +52,20 @@ Cross-references (read before implementing): - `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). + becomes a scaffold-level concern via a new optional `Ambient` slot (§3c, §3f). - `DeepDrftPublic.Client/Pages/MixDetail.razor`, `SessionDetail.razor`, `CutDetail.razor` — the three - release-detail hosts. + release-detail hosts (three distinct hosting modes — §3f). - `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). +- `DeepDrftAPI/Controllers/TrackController.cs`, `ReleaseController.cs` — the waveform endpoints; under the + per-track model the datum resolves for a **track** (§5), with the release as addressing context. - `DeepDrftContent/Processors/WaveformProfileService.cs`, `MixWaveformResolution.cs`, - `Constants/VaultConstants.cs` — the (content-agnostic) compute/store path and the two vaults. + `Constants/VaultConstants.cs` — the compute/store path and the vaults; **Direction B generalizes the + high-res compute here to every track** (§5). +- `DeepDrftAPI/Services/UnifiedTrackService.cs` — the upload path; Direction B adds a per-track high-res + compute at upload (§5, §8a-new). +- `DeepDrftManager` CMS generate action (the Mix-only "generate waveform" trigger) — Direction B + generalizes it off Mix-only to any track (§5). --- @@ -54,69 +76,82 @@ The visualizer is named `Mix*` throughout, but its *architecture* is release-gen | 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 | +| `MixWaveformVisualizer` bridge inputs | `ReleaseEntryKey` (string) + `TrackId` (long?) + cascaded player | **No** — already track-cardinal (keys on `TrackId`) | +| Playback coupling | `IsActivePlayer` gates on `TrackId` matching the cascaded player's current track | **No** — works for any 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 | +| **Datum fetch** | `IReleaseDataService.GetMixWaveform(entryKey)` → `GET api/release/{entryKey}/mix/waveform` | **Yes** — 404s unless `Medium == Mix`; resolves per *release*, not per *track* | +| **Datum source** | the high-res `mix-waveforms` vault, keyed by `MixMetadata.WaveformEntryKey` | **Yes** — only the Mix's single track gets 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. +`Medium == Mix` and resolves per-release*, and the *high-res datum that only Mix tracks 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. +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 **per-track** +datum source that exists for every track, 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 +track's `EntryKey` (this is the datum the player-bar `WaveformSeeker` already consumes). Mix tracks *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). +`mix-waveforms` vault, triggered by a CMS action. So **the high-res datum is the only gap** — and under +Daniel's per-track model it is a gap *per track*, not per release: every track should carry its own +high-res datum, computed at upload and (for existing tracks) backfilled. **Direction B (committed)** makes +the high-res compute a per-track compute for **all** media, keyed by track `EntryKey` (§5). The cheaper +road — serving the existing 512-bucket profile to non-Mix and skipping new compute (old "Direction A") — +was **the road not taken** (§5). --- ## 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). +its controls, mounted in **three hosting modes** (§3f) — *visualizer-is-the-page* on Mix detail, *ambient +environment* on Cut/Session detail, *contained live element* on the home-page NowPlaying card — each fed by +a **per-track high-res datum** that exists for every track (Direction B, §5). The visualizer always renders +**the currently playing/selected track's** datum; the release is the host, not the datum's owner. The lava +controls ride **every Release Detail host** — Mix, Cut, and Session all get the seven-knob bar + lava-lamp +toggle (full parity, §3d). The NowPlaying card drives the *same* engine off live playback (§6) and stays +**controls-suppressed by default** (the one open sub-question, §8b-followup). **In scope.** -- **Rename + relocate** the visualizer engine to a release-cardinal identity (`MixWaveformVisualizer` → +- **Rename + relocate** the visualizer engine to a track-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. +- **Generalize the high-res compute to every track (Direction B, §5).** This is now the load-bearing data + change — and it is **larger than a single endpoint**. It touches: (a) the **content** compute path + (`WaveformProfileService` / `MixWaveformResolution`) — generalize the duration-derived high-res compute + off Mix-only to any track; (b) the **upload** path (`UnifiedTrackService.UploadAsync`) — compute the + high-res per-track datum at upload for every new track; (c) the **CMS** generate action — generalize the + Mix-only "generate waveform" trigger to any track; (d) a **backfill** of the high-res datum for every + existing track that lacks one (§8a-new); (e) a **per-track datum fetch** endpoint (§5) the bridge calls. +- **Host the visualizer in three modes** — via a new optional `Ambient` slot on `ReleaseDetailScaffold` + for the Cut/Session ambient mode (§3c, §3f), Mix keeping its visualizer-is-the-page full-bleed mount, and + a contained mount on the NowPlaying card. The slot lets scaffold-composing pages mount without + re-implementing the 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. +- **Mount the lava controls on every detail-page host** — Mix, Cut, and Session all carry the seven-knob + bar + lava-lamp toggle (full parity, §3d). The NowPlaying card mounts **controls-suppressed** by default + (§3d, §8b-followup). **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. + changes its *input plumbing* and its *compute breadth*, 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 new high-res *algorithm*.** Direction B generalizes the *existing* duration-derived compute to run + per-track for all media; it does not redesign how the high-res datum is computed (`MixWaveformResolution` + carries forward — the same ~333 samples/sec duration-derived model). - **No playback-control change.** Read-only contract holds everywhere, including the NowPlaying card — the home card visualizes, it does not become a transport. @@ -136,7 +171,7 @@ every later wave references the generalized names: | `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) | +| `DDIcons.LavaLamp` / `LavaLampFilled` | keep (the lava-lamp glyph is the *controls* affordance, now on every detail-page host — §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 @@ -161,143 +196,247 @@ Generalizing does **not** mean flattening every medium to the same look. The cle 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. +`Header`/`Hero`/`TopRightAction` (Phase 9 §5.3) — extended to the ambient mount. -### 3c. The hosting seam — a `Backdrop` slot on `ReleaseDetailScaffold` (Wave 12.C) +### 3c. The hosting seam — an `Ambient` slot on `ReleaseDetailScaffold` (Wave 12.C) + +> **Naming note.** This slot was first drafted as `Backdrop`. Daniel pushed back: on the Mix page the +> visualizer is **not** a backdrop — it is the full-bleed centerpiece that *is* the page. "Backdrop" only +> describes what the visualizer does on Cut/Session (ambient environment *behind* the hero+content). The +> slot is named for *that* mode — the **ambient** mode — because Mix doesn't use the slot at all (it keeps +> its own visualizer-is-the-page mount; see §3f). So: `Ambient`, not `Backdrop`. 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: +Cut mounts nothing. To let Cut/Session host the visualizer as ambient environment DRY-ly: -**Add an optional `RenderFragment? Backdrop` slot to `ReleaseDetailScaffold`**, rendered as the +**Add an optional `RenderFragment? Ambient` 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). +container becomes the foreground stacking context — promote the `mix-detail-foreground` stacking pattern +into the scaffold so it is the default, not a Mix bespoke). A host that supplies no `Ambient` gets today's +plain background (Liskov: absent slot = no ambient layer, 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).** +- **Cut** (composes the scaffold) supplies `` to + `Ambient` **with the full seven-knob bar + lava-lamp toggle exposed** (full parity, §3d) — a living + waveform field behind the album hero + track list, rendering the *currently selected/playing track's* + datum, tunable in place. **§8b is resolved: Cut and Session get the ambient layer AND the controls + (full parity, not Mix-only).** The ambient *layout* (visualizer behind hero+content) is unchanged; the + difference from the earlier draft is that the controls are no longer suppressed here. +- **Session** does not compose the scaffold (§3e) — it mounts the ambient visualizer directly behind its + own hero overlay, the same engine, its own thin full-bleed wrapper. +- **Mix does *not* use this slot.** Mix is the *visualizer-is-the-page* mode (§3f) and keeps its existing + full-bleed mount + `TopRowCenter` knob-bar + lava-lamp toggle exactly as the Phase 10 reframe landed + them. The `Ambient` slot is for the *ambient* mode only — folding Mix into it would force the slot to + carry both "the page" and "behind the page," which is the conflation §3f exists to avoid. **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. +scaffold is where chrome lives. Putting the ambient layer 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) +### 3d. Where do the lava controls live per host? (the controls boundary — full parity, resolved) -The seven-knob lava bar is an **expert tuning surface** whose identity is "the lava lamp." Two clean -positions, and a recommendation: +The seven-knob lava bar is an **expert tuning surface** whose identity is "the lava lamp." **Daniel's §8b +call: full parity on the detail pages.** The controls ride **every Release Detail host**: -- **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). +- **Mix, Cut, and Session detail pages all carry the seven-knob bar + lava-lamp toggle.** The + `WaveformVisualizerControls` mount is **no longer Mix-suppressed** — it rides every detail-page mount. + On Mix (mode A) the bar sits in `TopRowCenter` over the full-bleed visualizer as the Phase 10 reframe + landed it; on Cut and Session (mode B) the same controls bar + lava-lamp toggle ride the ambient mount, + letting a visitor tune the living field behind the release. The *layout* still differs per mode (§3f) — + visualizer-is-the-page vs. ambient-behind-content — but the **controls presence is uniform across all + three detail hosts.** Rationale (Daniel): the lava is a signature affordance of the site; giving every + release page the tuning surface makes the whole site feel alive and consistent, not just the Mix page. + The shared `WaveformVisualizerControlState` supplies the dial values and is where each mount reads/writes, + so tuning behaves identically wherever the bar appears (see the state-scoping note below). +- **The NowPlaying home card (mode C) stays controls-suppressed by default.** Full parity was resolved + against the Release *Detail* pages; the NowPlaying card is the *contained* mode-C host — a small hero + panel, not a full page — where a seven-knob tuning bar may be awkward. Default: the card mounts the + ambient/contained field with **no knobs, no lava-lamp button**, rendering at the shared tuned defaults. + **This is the one open sub-question — see §8b-followup.** The seam supports controls-on-the-card either + way (the controls are a separate component over shared state), so flipping it later is a one-line + composition change, not an engine change. -**This is open question §8b.** Default to Mix-only controls unless Daniel says otherwise; the seam -supports either without an engine change. +> **State-scoping note (raised by full parity, staff-engineer's call at 12.C).** With the controls now +> present on Mix *and* Cut *and* Session, decide whether `WaveformVisualizerControlState` is **one shared +> tuning** (a knob turned on a Cut page is the same lava everywhere — single source of truth, simplest, and +> consistent with the "one engine" framing) or **per-host/per-mode scoped** (each surface remembers its own +> dial positions). **Recommend one shared state** — it matches the single-engine model, it is what the +> persisted `WaveformVisualizerControlState` already is, and "the lava lamp has one set of dials" is the +> simpler mental model. Flag only; not a product blocker. ### 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 +`MudContainer` + `ReleaseHeroOverlay`). If Session is to host the ambient layer 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 +Session gets the ambient layer at all — don't reopen the Session-vs-scaffold decision for this. Cut *does* +compose the scaffold, so Cut gets the ambient layer for free via the slot. This asymmetry is fine: the +slot serves scaffold-composing pages; 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. +variance §3b sanctions. **Under full parity (§3d), Session's direct mount also carries the seven-knob bar + +lava-lamp toggle** — the controls bar rides Session's own top-row composition the same way Cut's rides the +scaffold's, so Session is full-parity even though it doesn't compose the scaffold. + +### 3f. Three hosting modes of the one engine (the elaboration §8b asked for) + +Daniel's pushback on "backdrop": *"backdrop?? MIXES doesn't really have a backdrop?"* — and he is right. +The one engine is hosted in **three distinct modes**, and "backdrop" only fits one of them. Spelling them +out surface-by-surface, because the slot design and the naming follow from this distinction: + +| Mode | Surfaces | What the visualizer *is* on screen | Controls | Mount mechanism | +|------|----------|-----------------------------------|----------|-----------------| +| **A — Visualizer-is-the-page** | **Mix detail** | The full-bleed centerpiece. The lava field **is** the page; the mix details sit *over* it as a thin overlay. You came here to watch the lava. | Seven-knob bar + lava-lamp toggle (`TopRowCenter`) | Mix's own full-bleed mount (unchanged from Phase 10 reframe) — **not** the `Ambient` slot | +| **B — Ambient environment** | **Cut detail, Session detail** | Living texture *behind and around* the hero + content. The album cover / track list / session hero is the subject; the waveform is environment that makes the page feel alive. You came here for the release; the lava is atmosphere. | **Seven-knob bar + lava-lamp toggle (full parity, §3d)** | `ReleaseDetailScaffold.Ambient` slot (Cut) / direct full-bleed mount (Session, §3e) | +| **C — Contained live element** | **NowPlaying card** (home) | A small, bounded live panel *inside* the card. Not full-bleed, not "behind" anything — a contained element that shows the real signal of whatever is playing. | Suppressed by default (§8b-followup) | Contained mount, `Fill`-mode canvas sized to the card, not the viewport (§6c) | + +**Why three modes and not "backdrop everywhere":** the three differ in *what the user's eye treats as the +subject*. On Mix the visualizer **is** the subject (mode A). On Cut/Session the *release* is the subject +and the visualizer is environment (mode B — this is the only mode that is genuinely a "backdrop"). On the +home card the visualizer is a *bounded live readout* of current playback (mode C). Same engine, same datum +contract, same renderer — three compositions. This is the SOLID payoff stated precisely: the variance is +entirely in **hosting composition** (full-bleed vs. slot vs. contained; viewport-sized vs. container-sized; +controls present on all three detail hosts, suppressed-by-default only on the contained home card), never in +the engine. **Note the controls are no longer a per-mode discriminator on the detail pages** — modes A and B +both carry the full seven-knob bar (§3d, §8b resolved to full parity); only the mode-C home card suppresses +them by default (§8b-followup). + +**What this resolves about the slot.** Because Mix (mode A) keeps its own mount and the NowPlaying card +(mode C) is a contained mount, the `ReleaseDetailScaffold` slot serves **mode B only** — and that is why it +is named `Ambient`, not `Backdrop` and not `Visualizer`. A `Visualizer` slot would imply Mix routes through +it too; an `Ambient` slot says exactly what it carries: the behind-the-content environment layer for +scaffold-composing detail pages. Mode A and mode C mount the engine without the slot. + +**§8b status: RESOLVED — modes A + B + C all live, with FULL PARITY on the controls across all three detail +hosts (Daniel, 2026-06-17).** All three hosting modes ship (Mix is the page, Cut/Session are ambient, the +home card is contained) **and** the seven-knob lava bar + lava-lamp toggle ride every Release Detail host — +Mix, Cut, and Session — not Mix-only. The earlier "mode 1 (Mix + NowPlaying only, Cut/Session plain)" +fallback is closed; the earlier "controls Mix-only" default is overridden by Daniel's full-parity call. The +Cut/Session ambient treatment **plus its in-place tuning controls** is the distinctive-feel win the +generalization buys; the `Ambient` slot carries the field and the controls bar rides the host composition +alongside it, so each detail page is a one-mount-plus-controls-bar composition. **One sub-question remains +(§8b-followup):** whether full parity also extends the seven-knob bar onto the small NowPlaying home card +(mode C) — default **no** (the card stays controls-suppressed), flagged for Daniel. --- ## 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. +the rename: its datum fetch must resolve the high-res datum for the **current track**, not for a Mix +release. -Today: `await ReleaseData.GetMixWaveform(ReleaseEntryKey)` → 404 for non-Mix → blank backdrop. +Today: `await ReleaseData.GetMixWaveform(ReleaseEntryKey)` → resolves per-release, 404 for non-Mix → +blank. This is the per-*release* fetch the model correction supersedes. -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: +**Generalized (per-track model).** The visualizer renders *the currently playing/selected track's* +waveform, and the bridge already knows that track — it is the `TrackId` it gates `IsActivePlayer` on. So +the fetch should be keyed to the **track**, with the release as addressing context. Two shapes for the +fetch are viable; the spec recommends the first and leaves the exact route to staff-engineer (§5): -- 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. +- **Track-keyed (recommended):** `await ReleaseData.GetTrackWaveform(trackEntryKey)` → returns the + per-track high-res datum from the generalized waveform vault, keyed by the track's `EntryKey`. This is the + cleanest expression of "the datum is the track's." The bridge needs the current track's `EntryKey`; it + currently carries `TrackId` (the SQL PK) — see §5 on reconciling `TrackId` → `EntryKey`. +- **Release-addressed, track-resolved:** `GET api/release/{releaseEntryKey}/track/{trackId}/waveform` — the + release locates the track, the track owns the datum. Heavier route, but keeps the release as the + addressing root the bridge already holds (`ReleaseEntryKey`). Use this if resolving `TrackId` → `EntryKey` + client-side is awkward. + +The bridge stays otherwise identical: + +- **Re-fetch key changes from release to track.** The fetch-once guard must re-arm when the **track** + changes, not (only) when the release changes — because on a multi-track Cut the release is fixed while the + track scrolls. (Today's guard keys on `_loadedReleaseKey == ReleaseEntryKey`; under the per-track model it + keys on the current track identity.) This is the one bridge subtlety the model correction introduces — and + it is *exactly* the behavior the NowPlaying card already needs (§6c), so the two converge. +- Still derives duration from the cascaded player (`PlayerDurationSeconds`) — the duration source is the + *player*, which already reflects the current track, so the time↔sample mapping is correct per-track 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 `PlaybackPosition` composability fallback (the no-player-cascade path) 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)? +**No open data *question* remains here** — Direction B (committed, §5) means every track has a high-res +datum, so the fetch always resolves to high-res. The open *items* the model created are operational, not +design: the backfill (§8a-new) and the upload-time compute cost (§8b-new). --- -## 5. The datum source — the one real data decision +## 5. The datum source — Direction B, per-track (committed) -The visualizer renders a **loudness datum + a duration**. Two datums exist in the system today: +The visualizer renders a **loudness datum + a duration**. Daniel's model correction makes the datum +**per-track**: every track carries its own high-res waveform datum; the release is just the host. And +Daniel chose **Direction B**: compute that high-res datum for **all** media, not the 512-bucket fallback. +Together these settle §5 — no remaining product call here; the work is a generalization + a backfill. -| Datum | Vault | Resolution | Who has it | Keyed by | -|-------|-------|-----------|------------|----------| +**Datums in the system today:** + +| Datum | Vault | Resolution | Who has it today | 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`) | +| High-res datum | `mix-waveforms` | **~333 samples/sec** (duration-derived) | **Mix tracks 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:** +The 512-bucket profile already exists per-track and stays (the player-bar `WaveformSeeker` consumes it — +untouched). The high-res datum is the one the lava visualizer wants, and today only Mix tracks have it. +**Direction B closes that gap per-track for every medium.** -**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. +### 5a. The generalization (Direction B), end to end -**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 B is **not** just a new endpoint — under the per-track model it touches five surfaces: -**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. +1. **Content compute path.** Generalize the duration-derived high-res compute (`WaveformProfileService` + + `MixWaveformResolution`) off its Mix-only framing so it computes a per-track high-res datum for **any** + track from that track's audio. The *algorithm* is unchanged (the same ~333 samples/sec duration-derived + model `MixWaveformResolution` already defines); only its *applicability* widens from "the mix track" to + "any track." Store keyed by the track's `EntryKey`. +2. **Store / vault.** The high-res datum becomes per-track-cardinal. Either rename/repurpose the + `mix-waveforms` vault to a medium-neutral `track-waveforms` (cleaner; consistent with the rename + discipline) or keep the vault and drop the Mix-only key assumption — staff-engineer's call, but the + *name* should stop saying "mix" once it holds every track's datum. `MixMetadata.WaveformEntryKey` is no + longer the only pointer; the datum keys directly off the track's `EntryKey` (the same key the + `waveform-profiles` profile uses). +3. **Upload path.** `UnifiedTrackService.UploadAsync` already computes the 512-bucket profile for every + track at upload; **add the per-track high-res compute alongside it** so every *new* track gets its + high-res datum at upload time, no CMS step required. (Cost flagged — §8b-new.) +4. **CMS generate action.** Generalize the Mix-only "generate waveform" trigger to **any track** — so a + CMS operator can (re)generate a track's high-res datum on demand (the backfill mechanism, and the repair + path if a compute fails at upload). This is the action that drives the backfill (§8a-new). +5. **Backfill.** Every *existing* track that predates Direction B has a 512-bucket profile but no high-res + datum. Backfill computes the per-track high-res datum for all of them. **This is a real operational + step, Daniel-gated** — see §8a-new (does it run as a one-shot migration/script, or as a CMS batch + action over the generalized generate action?). -**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. +### 5b. The per-track datum fetch endpoint -**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. +Under the per-track model the fetch resolves a datum for a **track**, addressed with the release as +context. **Recommended shape:** `GET api/track/{trackEntryKey}/waveform` (track-cardinal, on +`TrackController`) returning the per-track high-res datum, 404 → graceful blank. This is the natural home +once the datum is the track's, and it sits beside the existing per-track surfaces. + +- **Reconciling with the bridge's identifiers.** The bridge today carries `ReleaseEntryKey` + `TrackId` + (the SQL PK), not the track's `EntryKey`. The fetch needs the track's `EntryKey`. Two ways to bridge: + (i) the player/track view-model the bridge already cascades exposes the current track's `EntryKey` + (preferred — the player knows the track it loaded, and `EntryKey` is how audio is keyed), so the bridge + reads `CurrentTrack.EntryKey` directly; or (ii) keep the release as the addressing root and resolve + server-side — `GET api/release/{releaseEntryKey}/track/{trackId}/waveform` — if the client can't cleanly + reach the track's `EntryKey`. **Recommend (i)**; it makes the route track-cardinal and matches the datum's + ownership. Final identifier plumbing is staff-engineer's, but the *route should be track-cardinal*. +- **`/mix/waveform` fate.** The Mix-gated `GET api/release/{entryKey}/mix/waveform` can be retired (folded + into the track-cardinal fetch — the Mix's single track resolves through the same path) or kept as a thin + delegate during transition. Staff-engineer's call; the spec's intent is **one** track-cardinal fetch. +- `IReleaseDataService` (or a track-cardinal `ITrackDataService` if cleaner) gains `GetTrackWaveform( + trackEntryKey)`; the bridge calls it. + +**The road not taken (one-line, per Daniel).** A cheaper v1 — serve the existing 512-bucket per-track +profile to non-Mix surfaces and skip new compute entirely ("Direction A") — was considered and **declined** +in favor of uniform high-res. Recorded so the trade isn't relitigated: A was zero-compute but coarse for the +lava; Daniel chose quality + uniformity over the cheaper road. --- @@ -322,23 +461,32 @@ the **live cascaded player**: *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. +- The datum comes from §5's per-track fetch keyed on the **current track** (`CurrentTrack.EntryKey`) — so + the home card shows the real high-res waveform of whatever track the listener started, Mix or not. This + is the cleanest illustration of the per-track model: the card has no "release" of its own to speak of; it + follows the *track*, and the datum is the track's. -**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. +**This is the payoff of the generalization:** the NowPlaying card is *just another host* (mode C, §3f) of +the same engine, pointed at "whatever track is playing right now." No NowPlaying-specific renderer, no fork +— the DRY win the brief demands. And because the datum is per-track and every track now has a high-res +datum (Direction B), the home card renders at full fidelity for *any* track, not just Mixes. ### 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 +- **Live, not static.** Unlike a single-track detail page, the NowPlaying card's **track** changes as + playback advances. Under the per-track model (§4) the fetch-once guard re-arms on **track** change (not + release change), so track-change → new per-track datum is handled — and this is the *same* guard change + the per-track model already requires for a multi-track Cut (where the release is fixed but the track + scrolls). The two converge on one behavior: **re-fetch when the current track's identity changes.** + Verify the guard keys on the current track, not the release. +- **Small surface, controls-suppressed by default.** The card is a small hero panel, not a full-bleed page. + Mount the visualizer **controls-suppressed** by default (no lava-lamp, no knob bar) and sized to the card, + not `position: fixed`. **Note this is now the *exception*, not the rule:** under Daniel's §8b full-parity + call the controls ride every Release *Detail* page (Mix, Cut, Session — §3d); the NowPlaying card is the + one host that suppresses them by default, because a seven-knob tuning bar on a small home-card may be + awkward. **Whether full parity extends here too is the open sub-question, §8b-followup** — the seam + supports it either way (the controls are a separate component over shared state). **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 @@ -364,64 +512,128 @@ The title/sub text block (`np-title`/`np-sub`) and the "Now Playing" label stay ## 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. +Sequenced so the mechanical rename de-risks everything, the per-track high-res generalization (now the +heaviest wave) unblocks the new hosts, and the NowPlaying rewire (the trickiest, per §6c) comes last on a +proven engine. **Direction B re-costs 12.B materially** — it is no longer "a new endpoint" but a +content + upload + CMS + backfill + fetch slice; the spec splits it into 12.B1 (compute generalization + +backfill) and 12.B2 (the bridge's per-track fetch) so the heavy data work and the thin client rewire can be +sequenced and tested apart. - **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. +- **12.B1 — Generalize the high-res compute to every track + backfill (the data change, Direction B).** + Generalize the duration-derived compute off Mix-only (`WaveformProfileService` / `MixWaveformResolution`), + store per-track keyed by `EntryKey` in the (renamed) `track-waveforms` vault, add the per-track high-res + compute to `UnifiedTrackService.UploadAsync`, generalize the CMS generate action to any track, and run the + **backfill** for existing tracks (§5a, §8a-new — backfill is Daniel-gated). **Independent of 12.A** (it is + server/content-side, no renamed client identifiers). Acceptance: every track — Mix, Session, and each Cut + track — has a high-res datum; new uploads get one automatically; the generate action works for any track. +- **12.B2 — Per-track datum fetch + bridge rewire.** New track-cardinal `GET + api/track/{trackEntryKey}/waveform` (§5b); `GetTrackWaveform`; bridge resolves the *current track's* + `EntryKey` and re-fetches on track-change (§4). **Depends on 12.A (renamed bridge) + 12.B1 (a datum to + fetch).** Acceptance: the Mix detail page renders the same high-res lava via the new track-cardinal fetch; + a non-Mix track now returns a high-res datum. +- **12.C — `Ambient` slot on the scaffold + mount on detail pages (mode B, §3f).** Promote the + full-bleed/foreground/footer-clip pattern into `ReleaseDetailScaffold` as an optional `Ambient` slot + (§3c); Cut mounts the ambient layer **with the full seven-knob bar + lava-lamp toggle** (full parity, + §3d); Session mounts directly **also full-parity** (§3e). Mix is **unchanged** (mode A keeps its own + mount + controls). **Depends on 12.B2** (a datum to render). **§8b is resolved (full parity, no longer + gated)** — Cut and Session ship with both the ambient layer and the controls. This wave also makes the + state-scoping call (§3d note — recommend one shared `WaveformVisualizerControlState`). Acceptance: Mix + unchanged; Cut and Session each show an ambient living layer **with a working seven-knob bar + lava-lamp + toggle**, rendering the current track's datum, no regression to the hero/content. +- **12.D — NowPlayingHero rewire (mode C, §3f).** Replace the synthetic bars with a contained, + controls-suppressed `` driven by the live player, pointed at the current track (§6); + add the `Fill`/container-sizing mode (§6c). **Depends on 12.A + 12.B2** (renamed engine + a per-track + datum for whatever's playing). **Independent of 12.C** (different host; doesn't need the scaffold slot). + Acceptance: the home card shows the *real* high-res 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. +**Dependency shape:** `12.A → 12.B2 → (12.C ‖ 12.D)`, with **12.B1 a parallel server-side track** that +12.B2 depends on (`12.B1 → 12.B2`) but that can start cold day one (it needs no renamed client identifiers). +12.A is the cheap mechanical unblock; **12.B1 is the new load-bearing heavy** (compute + backfill); 12.B2 is +the thin client rewire that consumes it; 12.C (detail-page ambient hosts, **full-parity controls**) and 12.D +(NowPlaying host, controls-suppressed by default) are independent siblings off 12.B2. The cold-start items +are **12.A** (touches everything, risks nothing) and **12.B1** (the data work — start it early; the backfill +gate is the only thing blocking it). With §8b resolved to full parity, 12.C no longer carries a mode-1 +fallback branch — Cut and Session are in, with controls. --- -## 8. Open product decisions (need Daniel before the dependent wave) +## 8. Open product decisions -**§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. +**Resolved by Daniel (2026-06-17), kept visible per file convention:** -**§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. +- **§8a (was: non-Mix datum resolution) → Direction B.** High-res compute for **all** media, not the + 512-bucket fallback. The cheaper "Direction A" (serve the existing 512-bucket profile to non-Mix) is the + road not taken (§5). Re-cost lands in 12.B1 (compute + backfill). +- **§8c (was: multi-track Cut's waveform) → dissolved by the per-track model.** The datum is per-track; the + visualizer renders the *current* track's datum, so there is no release-level "which track represents the + album" choice to make. The old "first track by `TrackNumber`" recommendation is moot. +- **§8b (Cut/Session hosting + controls) → RESOLVED: all three modes live, FULL PARITY on controls.** + Daniel chose **option (3) — full parity on the lava controls** (over option (2) Mix-only controls): the + visualizer rides every Release Detail host (Mix, Cut, Session) **and** the seven-knob bar + lava-lamp + toggle ride all three, not just Mix. The three-hosting-mode *layout* framing (§3f — visualizer-is-the-page + on Mix, ambient on Cut/Session, contained on NowPlaying) is retained; "backdrop" stays retired as the + wrong word for Mix. The only thing the flip changed: the `WaveformVisualizerControls` mount is **no longer + Mix-suppressed** on Cut/Session. The earlier mode-1 fallback (Mix + NowPlaying only) and the "controls + Mix-only" default are both closed. One sub-question spun out — §8b-followup below. -**§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. +**Newly open (created by the full-parity flip and by Direction B + the per-track model):** + +**§8b-followup — Do the full-parity controls extend onto the NowPlaying home card (mode C)?** +Daniel's §8b full-parity call (option 3) was answered against the Release *Detail*-page framing — Mix, Cut, +and Session all get the seven-knob bar + lava-lamp toggle. The **NowPlaying home card** is a different host: +the contained mode-C panel, a small hero card on the highest-traffic surface, **not** a full detail page. +The spec applies full-parity controls to the three detail pages and keeps the NowPlaying card +**controls-suppressed by default** (a living waveform readout with no knobs). Open: does Daniel want the +seven-knob bar on the home card too, or does it stop at the detail pages? **A seven-knob tuning bar on a +small now-playing card may be awkward** — it competes with the card's compact title/sub layout and puts an +expert affordance on the landing surface — which is why the default is *suppressed*. But the seam supports +it either way (the controls are a separate component over shared `WaveformVisualizerControlState`), so +flipping the card to controls-on is a one-line composition change in 12.D, not an engine change. +**Recommend keeping the card controls-suppressed** (full parity = detail pages; the home card stays a clean +ambient readout) — but this is a Daniel call, surfaced not buried. Affects only 12.D's mount; everything +else in the phase is settled by the full-parity resolution. + +**§8a-new — How does the high-res backfill run, and is it gated?** +Direction B means every *existing* track needs its per-track high-res datum computed once. Two shapes: +(1) a **one-shot migration/script** run against the content store (like the Daniel-gated EF migrations in +Phase 11 — authored, applied on Daniel's go-ahead); or (2) a **CMS batch action** over the generalized +generate action (an operator triggers "generate all," visible progress, re-runnable for failures). **Recommend +(2)** — it reuses the generalized generate action 12.B1 builds anyway, needs no separate one-shot tooling, +and the CMS already has the generate-status surface (Phase 8 §8.2's in-grid status column + generate +actions) to hang it on. **This is a Daniel-gated operational step either way** — like the Phase 11 release +migrations, the backfill is authored in 12.B1 but *run* on Daniel's word. Open: which shape, and when it +runs relative to shipping the per-track fetch (the fetch 404s gracefully for not-yet-backfilled tracks, so +the fetch can ship before the backfill completes — the visualizer just blanks until a track is backfilled). + +**§8b-new — Per-track high-res compute at upload: perf + storage cost (flag for Daniel).** +Direction B adds a duration-derived high-res compute (~333 samples/sec) to **every** track upload, on top +of the existing 512-bucket profile. Two costs to surface: +- **Upload latency.** The high-res compute walks the full audio at upload time. For a long Mix this is + already paid (Mixes compute it today); for short Cut tracks it is small. But it is now on the *upload hot + path* for every track, where before only the cheap 512-bucket profile was. If upload latency matters, + the compute could be **deferred** (upload returns; high-res computes async/queued, the generate action + the backfill uses doubling as the async worker) rather than inline. **Recommend: inline for now** + (simplest; the 512-bucket compute is already inline and the high-res isn't dramatically heavier for + typical tracks), flag deferral as the escape hatch if upload feels slow. +- **Storage.** A per-track high-res datum (~333 samples/sec × duration) is materially larger than the + fixed 512-bucket profile, and now *every* track stores one (a multi-track Cut stores N of them). For an + album of short tracks this is modest; for a library of long Sessions it adds up. **Not a blocker** — disk + is cheap and the datum is loudness samples, not audio — but flagged so the storage growth from "every + track gets a high-res datum" is a known consequence, not a surprise. **No action recommended;** surface + only. **§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. +the NowPlaying card (mode C, §3f) 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. --- @@ -429,20 +641,31 @@ the landing page. 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 +2. **Per-track high-res datum everywhere (Direction B).** Every track — Mix, Session, and each track of a + Cut — has a high-res duration-derived datum keyed by its `EntryKey`; new uploads get one automatically; + the generalized CMS generate action produces one for any track; the backfill has populated all + pre-existing tracks. +3. **Track-cardinal fetch.** `GET api/track/{trackEntryKey}/waveform` (or the agreed track-cardinal route) + returns the current track's high-res datum, 404 → graceful blank. The bridge resolves the *current + track* and re-fetches on track change. +4. **Mix unchanged (mode A).** The Mix detail page still renders the high-res lava with the seven-knob bar + + lava-lamp toggle, visualizer-is-the-page, at parity with the Phase 10 reframe — now via the + track-cardinal fetch. +5. **Ambient + controls on Cut/Session (mode B, full parity).** A Cut and a Session detail page show an + ambient living waveform layer rendering the *currently selected/playing track's* datum, **with a working + seven-knob bar + lava-lamp toggle** (full parity — controls are present and tunable in place, not + suppressed), with no regression to the hero/content. +6. **NowPlaying is real (mode C), controls-suppressed by default.** The home NowPlaying card shows the + *actual* high-res waveform of the playing track (scrolls/animates to the real signal, changes with track + changes), sits at-rest when nothing plays, and mounts **controls-suppressed by default** (no knob bar, no + lava-lamp — pending §8b-followup). No hardcoded synthetic bars remain. +7. **One engine, three modes.** Mix (mode A), the Cut/Session ambient layer (mode B), and the NowPlaying + card (mode C) all consume the *same* `WaveformVisualizer` component + renderer + state — verified by + there being exactly one of each, no per-host fork; the differences are hosting composition only + (full-bleed vs. `Ambient` slot vs. contained; viewport- vs. container-sized; controls present on all + three detail hosts, suppressed-by-default on the contained home card). +8. **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. +9. **Bridge intact, re-keyed to track.** Single-owner handle, idempotent datum guard, `IsActivePlayer` + gating, and the `isPlaying`-gated rAF loop are unchanged across all mounts; the datum guard now re-arms + on **track** change (the multi-track Cut and the NowPlaying card both rely on this — they converge).