Files
deepdrft/product-notes/phase-12-waveform-visualizer-generalization.md
daniel-c-harvey d6df0de63a docs(phase-12): fold popover-hosted controls into spec + plan
Controls move from an inline per-page knob bar to a single popover-hosted
panel triggered by the lava-lamp icon, placed identically on every host
(Mix, Cut, Session, NowPlaying card). Dissolves the NowPlaying-controls
question — full parity via the popover. Adds the popover panel wave, panel
styling from theme tokens, and a popover-anchor open item.
2026-06-17 06:07:49 -04:00

64 KiB
Raw Permalink Blame History

Phase 12 — Waveform Visualizer Generalization + NowPlayingHero Rewire (Design Spec)

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 controls 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 present on all three detail-page hosts, not suppressed on Cut/Session.

Controls-hosting revision (Daniel, 2026-06-17, supersedes the inline knob-bar model). "We have enough [controls] now that I want to design a panel to be hosted in a popover for the visualizer controls. The lava-lamp toggle should be wired to this popover, so anywhere we can put one Icon we can put the control surface." The eight knobs no longer ride an inline bar per page. They move into a single popover-hosted control panel triggered by the lava-lamp icon: click the icon → the panel pops over. This is more DRY than the per-page inline bar and it dissolves §8b-followup — with a popover, every host (Mix, Cut, Session, and the NowPlaying home card) places the same lava-lamp icon and gets the identical panel; full parity is achieved through the popover, not through a bar re-laid-out on each page. §8b-followup is now answered: the NowPlaying card gets the icon → popover like everywhere else. The panel adopts the NowPlaying Hero look (dark-navy ground, green-accent knobs, light icons, muted-navy filler — §3g). The §3/§6 hosting sections and the wave decomposition (§7, controls work consolidates into a distinct 12.E concern) are revised to this model. One new open question the popover creates — its positioning/anchor per host — is surfaced at §8e. 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 eight-knob controls) 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 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):

  • 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 eight-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; the controls component becomes the panel content hosted inside a new popover wrapper (§3d-revised, §3g) rather than an inline TopRowCenter bar. State + mapping otherwise unchanged.
  • New (this revision): a popover host component — working name WaveformVisualizerControlPopover — that pairs the lava-lamp trigger icon with the WaveformVisualizerControls panel as its overlay content. This is the single affordance every host places (§3d-revised). MudBlazor MudPopover (already in the dependency set) is the natural substrate. Styled to the NowPlaying Hero look (§3g) — no inline hex; tokens from DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css.
  • 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 Ambient slot (§3c, §3f).
  • DeepDrftPublic.Client/Pages/MixDetail.razor, SessionDetail.razor, CutDetail.razor — the three 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/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 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).

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 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; 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 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 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). 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 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 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 host — Mix, Cut, Session, and the NowPlaying card — via a single popover-hosted control panel triggered by the lava-lamp icon (§3d-revised). Every host places one icon and gets the identical panel; full parity (including the NowPlaying card) is achieved through the popover, so §8b-followup is closed. The NowPlaying card drives the same engine off live playback (§6).

In scope.

  • Rename + relocate the visualizer engine to a track-cardinal identity (MixWaveformVisualizerWaveformVisualizer, MixVisualizerControlsWaveformVisualizerControls, MixVisualizerControlStateWaveformVisualizerControlState, MixVisualizer.tsWaveformVisualizer.ts, MixZoomMappingWaveformZoomMapping). Pure renames; no behavior change. (§3a)
  • 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).
  • Build the popover-hosted control panel (§3d-revised, §3g). The renamed WaveformVisualizerControls becomes the panel content; a new WaveformVisualizerControlPopover host pairs the lava-lamp trigger icon with that panel as overlay content. One panel, one popover host, placed by an icon anywhere — the SOLID seam (§3d-revised). Style the panel to the NowPlaying Hero look (§3g) from real theme tokens.
  • Place the lava-lamp icon → popover on every host — Mix, Cut, Session, and the NowPlaying card. Every host's controls affordance is now identical: one icon, one popover, one panel. Full parity on all four surfaces (§3d-revised) — §8b-followup is dissolved by the popover.

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 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 eight knobs and …ControlState stay as-is (renamed). No new dials. The popover changes where the controls are hosted (a popover panel instead of an inline bar), not what they are — same knobs, same state, same Changed seam.
  • 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.

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 now the popover trigger — the single controls affordance, placed on every host — §3d-revised)
(new) WaveformVisualizerControlPopover.razor[.cs/.css] — popover host pairing the lava-lamp trigger icon with the WaveformVisualizerControls panel (§3d-revised, §3g). Not a rename; new in 12.E.

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, where the lava-lamp icon sits (the controls affordance is now a single popover-triggering icon — §3d-revised), 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 host that wants the controls places the lava-lamp icon and the identical popover panel follows.

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 ambient mount.

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 <MixWaveformVisualizer> then a .mix-detail-foreground wrapper, with the scaffold inside MudContainer). Session mounts nothing. Cut mounts nothing. To let Cut/Session host the visualizer as ambient environment DRY-ly:

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 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).

  • Cut (composes the scaffold) supplies <WaveformVisualizer ReleaseEntryKey=… TrackId=… /> to Ambient and places the lava-lamp icon → popover panel (full parity, §3d-revised) — a living waveform field behind the album hero + track list, rendering the currently selected/playing track's datum, tunable in place via the popover. §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 present, and (this revision) they live in a popover behind the lava-lamp icon rather than an inline bar.
  • 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 as the Phase 10 reframe landed it. The one controls change for Mix in this revision: its TopRowCenter inline knob-bar is replaced by the lava-lamp icon → popover panel (§3d-revised), the same affordance every other host now uses. The TopRightAction lava-lamp glyph Mix already mounts becomes the popover trigger directly — a small, contained change to Mix, not a redesign. 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 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? (the controls boundary — popover-hosted, full parity, resolved)

Revised model (Daniel, 2026-06-17): a single popover-hosted control panel, triggered by the lava-lamp icon. "We have enough [controls] now that I want to design a panel to be hosted in a popover for the visualizer controls. The lava-lamp toggle should be wired to this popover, so anywhere we can put one Icon we can put the control surface." This supersedes the inline knob-bar model (where each detail page laid out its own eight-knob bar). The controls are now one panel behind one icon, placed identically on every host.

The mechanics:

  • The lava-lamp icon is the single affordance. Click the icon → the control panel pops over. There is no separate inline bar and no separate toggle; the icon is the toggle and the popover is the panel. Closed state is just the icon (compact, unobtrusive). Open state floats the panel over the surface.
  • One panel, one popover host, placed by an icon anywhere. The renamed WaveformVisualizerControls becomes the panel content (the eight RadialKnobs, unchanged). A new WaveformVisualizerControlPopover host pairs the lava-lamp trigger icon with that panel as its overlay content (MudBlazor MudPopover the natural substrate). Every host renders exactly this one component; the panel is byte-for-byte identical everywhere. This is the SOLID seam, named precisely: one panel component, one popover host, placed by an icon anywhere.
  • Full parity across all four surfaces, the popover way. Mix, Cut, Session, and the NowPlaying card each place the lava-lamp icon and get the identical popover panel. There is no per-host control divergence left to decide — the variance that used to live in "which page shows the bar" is dissolved because every host shows the same icon. The only per-host question that remains is where the icon sits and where the popover anchors (§8e), which is positioning, not presence.
  • Why this is more DRY than the inline bar. The earlier model asked each detail page to host the controls bar in its own layout (Mix in TopRowCenter, Cut/Session in the ambient composition, the NowPlaying card suppressed because a bar didn't fit). That is three-to-four different control compositions over one shared state. The popover collapses them to one composition — <icon → popover → panel> — reused verbatim. The awkwardness that justified suppressing controls on the small NowPlaying card evaporates: a popover doesn't compete with the card's compact layout the way an inline bar would, because it's closed by default and floats when opened. That is what dissolves §8b-followup (below).

§8b-followup is dissolved, not deferred. The old open sub-question — "do the full-parity controls extend onto the small NowPlaying card?" — existed because an inline eight-knob bar was awkward on a compact card. With the popover, the card places the same lava-lamp icon as every other host and the panel floats on demand; there is no compact-layout conflict to weigh. Resolution: the NowPlaying card gets the icon → popover like everywhere else — full parity on all four surfaces. (See §8b-followup, now marked resolved.)

State-scoping note (staff-engineer's call at 12.E). With one shared panel surfaced on every host, WaveformVisualizerControlState should be one shared tuning — a knob turned in the popover on a Cut page is the same lava everywhere. This is even more natural under the popover than under the inline-bar model: there is literally one panel, so "one set of dials" is the only coherent reading. It matches the single-engine framing and is what the persisted WaveformVisualizerControlState already is. 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 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 <WaveformVisualizer> directly with its own full-bleed wrapper (small, local, mirrors what Mix does inline today). Recommend (b) if 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. Under full parity (§3d-revised), Session also places the lava-lamp icon → popover panel — the same single affordance every host uses. Because the controls are now one popover behind one icon (not an inline bar threaded into a page's top row), Session's lack of scaffold composition is a non-issue for controls: it just places the icon wherever its own chrome puts it. 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. Lava-lamp icon → popover panel (§3d-revised) 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. Lava-lamp icon → popover panel (full parity, §3d-revised) 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. Lava-lamp icon → popover panel (full parity — the popover dissolves the old suppression, §3d-revised) 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), never in the engine. The controls are no longer a per-mode discriminator at all — under the popover-hosted model (§3d-revised) all four surfaces place the same lava-lamp icon → popover panel. The only per-mode controls nuance left is where the icon anchors (§8e), which is positioning, not presence.

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. The controls popover is orthogonal to the slot — it's an icon any host places, independent of how the visualizer layer is mounted (slot vs. full-bleed vs. contained).

§8b status: RESOLVED — modes A + B + C all live, with FULL PARITY on the controls across all four hosts via the popover (Daniel, 2026-06-17). All three hosting modes ship (Mix is the page, Cut/Session are ambient, the home card is contained) and the lava-lamp controls ride every host — Mix, Cut, Session, and the NowPlaying card — via the single popover-hosted panel (§3d-revised). The earlier "mode 1 (Mix + NowPlaying only, Cut/Session plain)" fallback is closed; the earlier "controls Mix-only" default is overridden; and the "is the NowPlaying card controls-suppressed?" sub-question is dissolved by the popover (§8b-followup, now resolved). Each host is a one-visualizer-mount-plus-one-lava-lamp-icon composition. The remaining open item is the popover's anchor/positioning per host (§8e) — a layout detail, not a presence decision.

3g. Panel styling — the NowPlaying Hero look (theme tokens, no hardcoded hex)

Daniel's styling direction for the popover panel: "same look and feel as the NowPlaying Hero." Concretely:

  • Dark-navy primary background — the same navy ground the NowPlaying Hero card sits on.
  • Green-accent knobs with light-colored icons.
  • Muted-navy filler / circular border fill around the knobs.

These map directly onto the existing token layer (DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css — the single source of truth; the Phase 10 reframe's "one source, no hardcoded hexes" discipline holds). The mapping staff-engineer should pull from:

Panel element Daniel's intent Token (source of truth) Value (for reference only — use the token, not the hex)
Panel background dark-navy primary --deepdrft-navy (ground) / --deepdrft-navy-mid (elevated panel) #112338 / #17283f
Knob accent / active arc green accent --deepdrft-green-accent #3D7A68
Knob icons / labels light-colored --deepdrft-white #FAFAF8
Knob filler / circular border fill muted-navy --deepdrft-navy-mid (fill) / --deepdrft-muted (border-muted) #17283f / #8A9BB0
Panel border / divider subtle --deepdrft-border (or the dark-theme rgba(250,250,248,0.10) divider)

The NowPlaying Hero card itself (NowPlayingCard.razor.css) renders on a translucent off-white wash (rgba(250,250,248,0.06) over the page's navy ground, backdrop-filter: blur(8px)) with the green accent on its label/dot (--deepdrft-green-accent) and off-white title (--deepdrft-white). The panel should read as a sibling of that card — the same navy-ground/green-accent/off-white vocabulary, the same restrained translucency-over-navy feel — so a popover opened anywhere looks like it belongs to the same family as the home card. Staff-engineer pulls the tokens above; no inline hexes. If a token is missing for a needed shade (e.g. a knob's inactive track fill), prefer a color-mix() over the nearest token to a new hardcoded value, and flag the gap rather than minting an untokened hex.

Note on the MudBlazor palette. The MudBlazor theme (DeepDrftShared.Client/Common/DeepDrftPalettes.cs, applied via <MudThemeProvider Theme="DeepDrftPalettes.Default">) carries the same navy/green/off-white vocabulary in its PaletteDark (Primary = #3D7A68 green-accent, Background = #0D1B2A navy, Surface = #162437 navy-mid, TextPrimary = #FAFAF8 off-white). For MudBlazor-component-level theming inside the popover, prefer Color="Color.Primary"/Color.Surface against that palette over CSS overrides where the component supports it; reach for the CSS --deepdrft-* tokens for the bespoke knob/panel chrome the RadialKnob owns. Either way the colors trace back to one source — the tokens file and the palette are the same vocabulary — so there is no third place to hardcode.


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 the high-res datum for the current track, not for a Mix release.

Today: await ReleaseData.GetMixWaveform(ReleaseEntryKey) → resolves per-release, 404 for non-Mix → blank. This is the per-release fetch the model correction supersedes.

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):

  • 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 TrackIdEntryKey.
  • 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 TrackIdEntryKey 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) stays as the documented escape hatch.

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 — Direction B, per-track (committed)

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.

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
High-res datum mix-waveforms ~333 samples/sec (duration-derived) Mix tracks only (CMS-triggered) MixMetadata.WaveformEntryKey (= the mix track's EntryKey)

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.

5a. The generalization (Direction B), end to end

Direction B is not just a new endpoint — under the per-track model it touches five surfaces:

  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?).

5b. The per-track datum fetch endpoint

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.


6. Overhauling the NowPlayingHero (Wave 12.D)

6a. What's there now

NowPlayingCard.razor shows the now-playing title/sub plus, when Player.IsLoaded, a waveform-bars div of 20 hardcoded <div class="waveform-bar"> elements, each with fixed --h-lo/--h-hi/--dur CSS custom properties driving a CSS keyframe bounce. It is purely synthetic — no audio data, no coupling to the actual signal; it bounces on a fixed loop whenever anything is loaded. This is the "stochastic bullshit."

6b. The rewire — mount the real engine, driven by live playback

Replace the waveform-bars block with a mounted <WaveformVisualizer> scoped to the card, driven by the live cascaded player:

  • The NowPlaying card already cascades IStreamingPlayerService (it reads Player.CurrentTrack).
  • Mount <WaveformVisualizer ReleaseEntryKey="@Player.CurrentTrack.Release?.EntryKey" TrackId="@Player.CurrentTrack.Id" /> — the same bridge, the same playback coupling. Because the card is inside the cascade, 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 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 (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 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, popover controls (full parity via the icon). The card is a small hero panel, not a full-bleed page — but under the popover-hosted model (§3d-revised) that is no longer a reason to suppress controls. The card places the same lava-lamp icon → popover panel as every other host; the panel floats over the page on demand and doesn't compete with the card's compact title/sub layout the way an inline eight-knob bar would have. This is what dissolved §8b-followup — the popover removed the awkwardness that justified suppression. The card's lava-lamp icon sits in/near the card chrome (its anchor is one of the §8e positioning calls — a small card is the tightest anchor case). The visualizer canvas is still sized to the card, not position: fixed (§6c container-sizing below). 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 card's mount can run a cheaper preset (fewer blobs) via a density default — but do not fork the renderer; use the existing density dial (now adjustable from the card's own popover too). 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 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.

The controls work now consolidates into its own wave, 12.E (the popover-panel revision). Under the inline-bar model the controls were threaded into each detail-page mount, so 12.C carried "ambient layer + controls bar" together. The popover model pulls the controls out of the per-host mounts into one shared component built once (12.E) and placed by each host (an icon, §3d-revised). This is cleaner: 12.C and 12.D each reduce to "mount the visualizer layer + place the lava-lamp icon," and the panel/popover/styling all live in one wave. 12.E depends only on the rename (12.A) — it touches the controls component + a new popover host + the panel styling, none of which need the per-track fetch — so it can run in parallel with the data work (12.B1/12.B2).

  • 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.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.E — Popover-hosted control panel (the controls revision). Turn the renamed WaveformVisualizerControls into the panel content and build the WaveformVisualizerControlPopover host pairing the lava-lamp trigger icon with that panel as overlay content (MudPopover, §3d-revised). Style the panel to the NowPlaying Hero look from theme tokens (§3g — no hardcoded hex). Make the state-scoping call (one shared WaveformVisualizerControlState, §3d-revised note). Depends on 12.A (renamed controls component) only — no per-track datum needed, so it runs parallel to 12.B. Acceptance: the lava-lamp icon opens a popover panel with all eight knobs, styled to the Hero look; turning a knob drives the visualizer (where one is mounted) via the unchanged Changed seam; the panel is one component reused everywhere. This wave is the unit every host then places (§3d-revised).
  • 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 and places the lava-lamp icon → popover (full parity, §3d-revised); Session mounts directly also full-parity (§3e). Mix is unchanged as a visualizer layer (mode A keeps its own full-bleed mount); its only controls change is swapping the inline TopRowCenter bar for the lava-lamp icon → popover (folded into 12.E's affordance). Depends on 12.B2 (a datum to render) and 12.E (the popover to place). §8b is resolved (full parity, no longer gated) — Cut and Session ship with both the ambient layer and the popover controls. Acceptance: Mix unchanged as a layer; Cut and Session each show an ambient living layer with a working lava-lamp icon → popover panel, 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 <WaveformVisualizer> driven by the live player, pointed at the current track (§6); add the Fill/container-sizing mode (§6c); place the lava-lamp icon → popover on the card (full parity, §6c — the popover dissolves the old suppression). Depends on 12.A + 12.B2 + 12.E (renamed engine + a per-track datum + the popover to place). 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, sits at-rest when nothing plays, and carries the lava-lamp icon → popover like every other host; no synthetic bars remain.

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), and 12.E (the popover controls) a third parallel track depending only on 12.A — both 12.C and 12.D now also depend on 12.E to place the icon. So: 12.A → {12.B1 → 12.B2, 12.E}, then (12.B2 ∧ 12.E) → (12.C ‖ 12.D). 12.A is the cheap mechanical unblock; 12.B1 is the load-bearing heavy (compute + backfill); 12.B2 is the thin client rewire that consumes it; 12.E is the controls consolidation (one panel + popover + styling, built once); 12.C (detail-page ambient hosts) and 12.D (NowPlaying host) are independent siblings that each mount a visualizer layer and place the popover. The cold-start items are 12.A (touches everything, risks nothing), 12.B1 (the data work — start it early; the backfill gate is the only thing blocking it), and 12.E (the controls panel — independent of all the data work). With §8b resolved to full parity and the popover dissolving §8b-followup, 12.C and 12.D carry no controls-suppression branch — every host places the same icon.


8. Open product decisions

Resolved by Daniel (2026-06-17), kept visible per file convention:

  • §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, now POPOVER-HOSTED. 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 controls ride all three. 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. Controls-hosting revision (2026-06-17): the controls are no longer an inline knob bar per page — they are a single popover-hosted panel triggered by the lava-lamp icon, placed identically on every host (§3d-revised). The earlier mode-1 fallback (Mix + NowPlaying only) and the "controls Mix-only" default are both closed. The sub-question that spun out (§8b-followup) is now resolved by the popover (below).

  • §8b-followup (do full-parity controls extend onto the NowPlaying card?) → RESOLVED by the popover. This sub-question existed because an inline eight-knob bar was awkward on a small, compact, high-traffic card — so the earlier draft kept the card controls-suppressed by default and flagged the question for Daniel. The popover-hosting revision dissolves it: the card places the same lava-lamp icon as every other host, and the panel floats on demand rather than occupying the card's layout. There is no compact-layout conflict left to weigh. Resolution: the NowPlaying card gets the icon → popover like everywhere else — full parity on all four surfaces (Mix, Cut, Session, NowPlaying card). The only remaining controls nuance is where the icon anchors per host — a positioning detail, now §8e.

Newly open (created by the popover revision and by Direction B + the per-track model):

§8e — Popover anchor/positioning per host (created by the popover revision). The popover dissolves the presence question but creates a placement one: where does the lava-lamp trigger icon sit, and where does the panel anchor, on each host? The hosts differ enough that one anchor rule may not fit all:

  • Mix (mode A, full-bleed) — the icon can take Mix's existing TopRightAction slot (it already mounts the lava-lamp glyph there); the panel anchors off that corner over the full-bleed field. Cleanest case.
  • Cut/Session (mode B, ambient) — the icon rides the page chrome (Cut via the scaffold, Session via its own composition); the panel floats over hero+content. Needs to clear the hero overlay and not collide with the share/play affordances already in the top row.
  • NowPlaying card (mode C, small contained) — the tightest case. A small card has little room for an icon, and a popover panel sized to the Hero look may be larger than the card itself — so the panel likely anchors to the icon but overflows the card bounds (which is fine — popovers float above), yet its open position must not cover the title/sub it's meant to accompany. Worth a deliberate anchor choice (e.g. open upward/leftward away from the card body). Recommend: a single WaveformVisualizerControlPopover with a per-host anchor origin parameter (MudPopover already exposes AnchorOrigin/TransformOrigin), defaulted sensibly and overridden per host — not a forked popover. This keeps one component while letting each host place it well. Staff-engineer-owned layout call; flagged for Daniel only because the card case (mode C) may look cramped and is worth a glance in review. Does not block the phase; the default anchor is shippable and tunable.

§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 (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.


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. 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 as a layer (mode A); controls via popover. The Mix detail page still renders the high-res lava visualizer-is-the-page at parity with the Phase 10 reframe — now via the track-cardinal fetch. Its controls are reached through the lava-lamp icon → popover panel (replacing the inline TopRowCenter bar), the same affordance every other host uses.
  5. Ambient + popover 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 lava-lamp icon → popover control panel (full parity — controls present and tunable in place via the popover), with no regression to the hero/content.
  6. NowPlaying is real (mode C), with popover controls (full parity). 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 carries the lava-lamp icon → popover panel like every other host (§8b-followup resolved by the popover). No hardcoded synthetic bars remain.
  7. One popover-hosted control panel, placed everywhere. There is exactly one WaveformVisualizerControls panel and one WaveformVisualizerControlPopover host; Mix, Cut, Session, and the NowPlaying card all place the same lava-lamp icon → popover and get the identical panel — verified by no per-host control fork. The panel is styled to the NowPlaying Hero look (§3g) from theme tokens, no hardcoded hex.
  8. 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 are uniform across all four hosts (the single popover panel, criterion 7).
  9. 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. The popover panel exposes only the eight tuning dials — no transport.
  10. 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).