refactor(12.A): rename Mix* visualizer engine to Waveform* abstraction

This commit is contained in:
daniel-c-harvey
2026-06-17 10:16:44 -04:00
parent ad94354632
commit 3839948eeb
12 changed files with 58 additions and 58 deletions
@@ -29,7 +29,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
// error banner.
//
// _miniDock is the minimized FAB container. We observe it in minimized state so
// --player-height stays non-zero (the FAB's actual height) and the MixWaveformVisualizer
// --player-height stays non-zero (the FAB's actual height) and the WaveformVisualizer
// clips to the top of the FAB rather than extending to the viewport bottom (fix §1).
// The player-spacer's .minimized class uses a hardcoded 60px and ignores the var,
// so publishing the FAB height here does not regress the spacer.
@@ -125,7 +125,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
// The Fixed embed is already in normal flow — no spacer/clip needed.
// For the docked player: we observe in BOTH expanded and minimized states
// so --player-height always reflects the live height of whichever element
// is visible. This keeps the MixWaveformVisualizer clipped to the top of
// is visible. This keeps the WaveformVisualizer clipped to the top of
// the footer in both states (fix §1).
// expanded → observe _playerRoot (full player bar, reflows across breakpoints)
// minimized → observe _miniDock (floating FAB container, ~5660px)
@@ -1,16 +1,16 @@
@namespace DeepDrftPublic.Client.Controls
@* Full-page scrolling Mix waveform background (Phase 9, 8.K). A windowed slice of the mix's loudness
@* Full-page scrolling waveform background (Phase 9, 8.K). A windowed slice of the track's loudness
datum scrolls bottom-to-top, coupled to playback; a zoom slider controls the visible time-span (and
so the apparent scroll speed, Guitar-Hero style). Strictly read-only: it self-fetches its datum from
ReleaseEntryKey, takes playback as one-way input only, and never seeks or writes back. The rAF loop and all
scroll/zoom/compositing math live in the MixVisualizer.ts interop module; this component is a thin
scroll/zoom/compositing math live in the WaveformVisualizer.ts interop module; this component is a thin
bridge that feeds it datum + playback + zoom + theme. Deliberately NOT the player-bar peak-bar idiom. *@
<div class="mix-waveform-bg">
<canvas @ref="_canvas" class="mix-waveform-canvas"></canvas>
</div>
@* The viewing controls (resolution + the three Wave 2 controls) live in MixVisualizerControls,
@* The viewing controls (resolution + the three Wave 2 controls) live in WaveformVisualizerControls,
rendered in the mix-detail foreground row below the back button — NOT here. This component is now a
pure backdrop bridge; it pushes uniforms in response to the shared MixVisualizerControlState. *@
pure backdrop bridge; it pushes uniforms in response to the shared WaveformVisualizerControlState. *@
@@ -8,10 +8,10 @@ using Microsoft.JSInterop;
namespace DeepDrftPublic.Client.Controls;
/// <summary>
/// Full-page scrolling Mix waveform background. Standalone and reusable: give it a
/// Full-page scrolling waveform background. Standalone and reusable: give it a
/// <see cref="ReleaseEntryKey"/> and it fetches its own loudness datum. The rendering itself — a windowed,
/// bottom-to-top, playback-coupled scroll with a glassy theme-aware gradient — lives in the
/// MixVisualizer.ts interop module; this component is the bridge that feeds it datum, playback
/// WaveformVisualizer.ts interop module; this component is the bridge that feeds it datum, playback
/// position, zoom, and theme, and owns the module lifecycle.
///
/// Strictly read-only (spec §D): no seek, no two-way write-back. <see cref="PlaybackPosition"/> is a
@@ -20,12 +20,12 @@ namespace DeepDrftPublic.Client.Controls;
/// <see cref="PlaybackPosition"/> parameter is the composability fallback for hosts that have no
/// player cascade (e.g. an embed) and want to drive position themselves.
/// </summary>
public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
{
[Inject] public required IReleaseDataService ReleaseData { get; set; }
[Inject] public required IJSRuntime JS { get; set; }
[Inject] public required MixVisualizerControlState ControlState { get; set; }
[Inject] public required ILogger<MixWaveformVisualizer> Logger { get; set; }
[Inject] public required WaveformVisualizerControlState ControlState { get; set; }
[Inject] public required ILogger<WaveformVisualizer> Logger { get; set; }
// Live playback + the mix duration come from the cascaded streaming player when present. The
// cascade is IsFixed, so we subscribe to its multicast StateChanged side-channel to learn about
@@ -54,12 +54,12 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
/// </summary>
[Parameter] public double PlaybackPosition { get; set; }
// Bridge-level diagnostics. Mirrors the JS-side DEBUG flag in MixVisualizer.ts: when true the
// Bridge-level diagnostics. Mirrors the JS-side DEBUG flag in WaveformVisualizer.ts: when true the
// datum-fetch / subscription / playback-coupling seams log to the browser console (prefixed
// `[MixVisualizer]`, same as the JS logs so the two interleave into one timeline). These pinpoint
// `[WaveformVisualizer]`, same as the JS logs so the two interleave into one timeline). These pinpoint
// which upstream link is broken when the ribbon stays blank — set true temporarily to diagnose.
private static readonly bool Debug = false;
private const string Tag = "[MixVisualizer]";
private const string Tag = "[WaveformVisualizer]";
private static void DebugLog(string message)
{
@@ -164,12 +164,12 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
try
{
_module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./js/visualizer/MixVisualizer.js");
"import", "./js/visualizer/WaveformVisualizer.js");
_handle = await _module.InvokeAsync<IJSObjectReference>("create", _canvas);
}
catch (JSException ex)
{
Logger.LogWarning(ex, "MixWaveformVisualizer: failed to load the visualizer module; rendering a plain backdrop.");
Logger.LogWarning(ex, "WaveformVisualizer: failed to load the visualizer module; rendering a plain backdrop.");
return;
}
@@ -203,7 +203,7 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
/// and to re-push when the controls bar signals a change. Each value is its own dedicated dial:
/// <list type="bullet">
/// <item>scroll speed [0,1] is mapped onto the useful zoom band via
/// <see cref="MixZoomMapping.ScrollKnobToSeconds"/> and pushed through <c>setScrollSpeed</c>
/// <see cref="WaveformZoomMapping.ScrollKnobToSeconds"/> and pushed through <c>setScrollSpeed</c>
/// (higher speed → tighter window → faster scroll);</item>
/// <item>gradient rotation speed → <c>setGradientRotationSpeed</c> (live OKLab anchor rotation);</item>
/// <item>gravity / heat / collision strength → their dedicated lava-physics dials;</item>
@@ -218,7 +218,7 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
if (_handle is null) return;
// Scroll speed is a normalized [0,1] axis; map it onto the useful zoom band (Phase 10 retune —
// the knob's full travel now covers the 60%100% zoom range, dropping the dead slow/wide end).
var visibleSeconds = MixZoomMapping.ScrollKnobToSeconds(ControlState.ScrollSpeed);
var visibleSeconds = WaveformZoomMapping.ScrollKnobToSeconds(ControlState.ScrollSpeed);
await _handle.InvokeVoidAsync("setScrollSpeed", visibleSeconds);
await _handle.InvokeVoidAsync("setGradientRotationSpeed", ControlState.GradientRotationSpeed);
await _handle.InvokeVoidAsync("setLavaGravity", ControlState.LavaGravity);
@@ -1,8 +1,8 @@
@namespace DeepDrftPublic.Client.Controls
@using DeepDrftPublic.Client.Services
@inject MixVisualizerControlState ControlState
@inject WaveformVisualizerControlState ControlState
@* The Mix visualizer controls. EIGHT continuous RadialKnobs — scroll speed, gradient rotation speed,
@* The waveform visualizer controls. EIGHT continuous RadialKnobs — scroll speed, gradient rotation speed,
lava gravity, lava heat, fluid amount, fluid viscosity, collision strength, waveform width — each its
own dedicated control with a Material-icon caption. The single "bubbles" knob is split into
fluid-amount + fluid-viscosity (Phase 10 §5).
@@ -14,8 +14,8 @@
convention) — the knobs are simply not rendered when hidden, while a min-height container holds the
layout. No collapse animation, no glass surface, no CSS visibility-hiding of populated knobs.
It owns NO JS interop: it mutates the shared, session-scoped MixVisualizerControlState and raises its
Changed event. The backdrop bridge (MixWaveformVisualizer) subscribes to that event and pushes the
It owns NO JS interop: it mutates the shared, session-scoped WaveformVisualizerControlState and raises its
Changed event. The backdrop bridge (WaveformVisualizer) subscribes to that event and pushes the
affected dial to the WebGL module. That keeps the JS module handle single-owned by the bridge and
this component purely presentational. None of these is a seek surface (read-only contract §D).
@@ -1,13 +1,13 @@
namespace DeepDrftPublic.Client.Controls;
/// <summary>
/// Pure mapping between the Mix visualizer's zoom slider position [0, 1] and the visible time-span in
/// Pure mapping between the waveform visualizer's zoom slider position [0, 1] and the visible time-span in
/// seconds. The span range is wide (0.333 s … 30 s, ~90×), so the mapping is logarithmic — equal
/// slider travel changes the span by an equal *ratio*, which feels even to the hand. Slider
/// orientation: fraction 0 = most zoomed-out (longest span), fraction 1 = most zoomed-in (the
/// 0.333 s quarter-note-@-180-BPM anchor). Extracted from the component so the math is unit-testable.
/// </summary>
public static class MixZoomMapping
public static class WaveformZoomMapping
{
/// <summary>Shortest span (max zoom): one quarter note at 180 BPM = 60/180 s. Hard anchor.</summary>
public const double MinVisibleSeconds = 60.0 / 180.0;
@@ -1,6 +1,6 @@
.deepdrft-footer {
/* position:relative + z-index:1 creates a stacking context that paints above the
MixWaveformVisualizer backdrop (z-index:0), keeping footer text fully legible. */
WaveformVisualizer backdrop (z-index:0), keeping footer text fully legible. */
position: relative;
z-index: 1;
background: var(--deepdrft-white);
+5 -5
View File
@@ -36,7 +36,7 @@ else
@* Full-page waveform sits behind the scaffold content. The scaffold's container is positioned
above it via the mix-detail-foreground stacking context. TrackId lets the visualizer couple to
playback only when the player is on this mix's track. *@
<MixWaveformVisualizer ReleaseEntryKey="@release.EntryKey" TrackId="@ViewModel.Track?.Id" />
<WaveformVisualizer ReleaseEntryKey="@release.EntryKey" TrackId="@ViewModel.Track?.Id" />
<div class="mix-detail-foreground">
<MudContainer MaxWidth="MaxWidth.Large" Class="mix-detail-container">
@@ -57,9 +57,9 @@ else
Phase 10 §4: the control is ALWAYS rendered; the lava-lamp toggle feeds its Visible
parameter, and the control itself @if-gates the knobs while holding the container's
reserved height — so content below never pops on toggle. The band mutates the shared
MixVisualizerControlState; the backdrop bridge pushes the dials. A knob drag does not
WaveformVisualizerControlState; the backdrop bridge pushes the dials. A knob drag does not
toggle it — the lamp's click does. *@
<MixVisualizerControls Visible="@_controlsExpanded" />
<WaveformVisualizerControls Visible="@_controlsExpanded" />
</TopContent>
<TopRightAction>
@* Lava-lamp button top-right, across from the back link. Toggles the knob band below the
@@ -126,8 +126,8 @@ else
}
}
// Lava-lamp knob-band visibility. Pure presentation over MixVisualizerControlState — gates whether
// the seven-knob MixVisualizerControls is rendered into the TopContent band; toggling it touches no
// Lava-lamp knob-band visibility. Pure presentation over WaveformVisualizerControlState — gates whether
// the seven-knob WaveformVisualizerControls is rendered into the TopContent band; toggling it touches no
// control value or bridge push. The lava-lamp button's filled/outline glyph is driven off this flag.
private bool _controlsExpanded;
@@ -1,7 +1,7 @@
namespace DeepDrftPublic.Client.Services;
/// <summary>
/// Holds the Mix visualizer's eight continuous-control positions for the lifetime of the WASM app
/// Holds the waveform visualizer's eight continuous-control positions for the lifetime of the WASM app
/// instance. Scoped in DI, so it lives across SPA navigations within one listening session — open a
/// second mix and the knobs keep where you left them — but a fresh page load (F5) constructs a new
/// instance, resetting to defaults. That matches the spec's "persist within session, reset on fresh
@@ -11,27 +11,27 @@ namespace DeepDrftPublic.Client.Services;
/// parameters: this is a plain scoped value holder, so widening it (the Phase 10 split of the single
/// density knob into fluid-amount + fluid-viscosity) adds fields + defaults only and never forces a
/// consumer constructor to grow. Each C#-side default mirrors a TS-side tuning anchor in
/// MixVisualizer.ts; keep the two in sync, as the <c>DefaultVisibleSeconds</c> /
/// WaveformVisualizer.ts; keep the two in sync, as the <c>DefaultVisibleSeconds</c> /
/// <c>DEFAULT_VISIBLE_SECONDS</c> pair does.
///
/// <para>
/// <see cref="Changed"/> is the decoupling seam between the controls bar and the visualizer bridge.
/// The controls component (a sibling of the backdrop in the page tree) only mutates this shared state
/// and raises <see cref="Changed"/>; the bridge component (<c>MixWaveformVisualizer</c>) subscribes
/// and raises <see cref="Changed"/>; the bridge component (<c>WaveformVisualizer</c>) subscribes
/// and pushes the affected uniform/dial to the JS module. This keeps the JS module handle single-owned
/// by the bridge — no handle sharing, no service-locator, no cross-component interop.
/// </para>
/// </summary>
public sealed class MixVisualizerControlState
public sealed class WaveformVisualizerControlState
{
// ── The eight control defaults (Phase 10). Each mirrors a DEFAULT_* anchor in
// MixVisualizer.ts; keep the two in sync, as the default-sync discipline requires.
// WaveformVisualizer.ts; keep the two in sync, as the default-sync discipline requires.
// Feel-anchors only — Daniel tunes on screen; the ~20% gravity / ~100% heat pair is his stated
// sweet spot (§4c).
/// <summary>
/// Default scroll-speed dial. Normalized [0,1] → mapped C#-side to a visible time-span in seconds
/// via <see cref="MixZoomMapping"/>, then sent to MixVisualizer.ts as a seconds value via
/// via <see cref="WaveformZoomMapping"/>, then sent to WaveformVisualizer.ts as a seconds value via
/// <c>setScrollSpeed</c>. The TS-side anchor is <c>DEFAULT_VISIBLE_SECONDS</c>. Opens mid-range
/// (0 = slow/wide window, 1 = fast/tight window).
/// </summary>
@@ -39,26 +39,26 @@ public sealed class MixVisualizerControlState
/// <summary>
/// Default gradient-rotation-speed dial. Mirrors <c>DEFAULT_GRADIENT_ROTATION_SPEED</c> in
/// MixVisualizer.ts. Normalized [0,1] → slow→fast anchor-rotation; drives the live OKLab
/// WaveformVisualizer.ts. Normalized [0,1] → slow→fast anchor-rotation; drives the live OKLab
/// three-colour gradient. 0.45 opens with a clearly-visible ~7 s colour cycle (Phase 10 §3.2).
/// </summary>
public const double DefaultGradientRotationSpeed = 0.45;
/// <summary>
/// Default lava-gravity dial. Mirrors <c>DEFAULT_LAVA_GRAVITY</c> in MixVisualizer.ts. Normalized
/// Default lava-gravity dial. Mirrors <c>DEFAULT_LAVA_GRAVITY</c> in WaveformVisualizer.ts. Normalized
/// [0,1]; 0 = near-weightless float, 1 = wax falls + settles fast. Tuned to Daniel's ~20% sweet spot.
/// </summary>
public const double DefaultLavaGravity = 0.2;
/// <summary>
/// Default lava-heat dial. Mirrors <c>DEFAULT_LAVA_HEAT</c> in MixVisualizer.ts. Normalized [0,1];
/// Default lava-heat dial. Mirrors <c>DEFAULT_LAVA_HEAT</c> in WaveformVisualizer.ts. Normalized [0,1];
/// 0 = wax rests at the bottom (collision-only), 1 = many small turbulent rising bubbles. Tuned to
/// Daniel's ~100% sweet spot.
/// </summary>
public const double DefaultLavaHeat = 1.0;
/// <summary>
/// Default fluid-amount dial. Mirrors <c>DEFAULT_FLUID_AMOUNT</c> in MixVisualizer.ts. The first
/// Default fluid-amount dial. Mirrors <c>DEFAULT_FLUID_AMOUNT</c> in WaveformVisualizer.ts. The first
/// half of the Phase 10 "bubbles" split. Normalized [0,1]; 0 = few small blobs, 1 = many larger
/// blobs (more wax in the container — blob count + per-blob volume).
/// </summary>
@@ -66,26 +66,26 @@ public sealed class MixVisualizerControlState
/// <summary>
/// Default fluid-viscosity / cohesion dial. Mirrors <c>DEFAULT_FLUID_VISCOSITY</c> in
/// MixVisualizer.ts. The second half of the Phase 10 "bubbles" split. Normalized [0,1]; 1 = high
/// WaveformVisualizer.ts. The second half of the Phase 10 "bubbles" split. Normalized [0,1]; 1 = high
/// cohesion (crisp spheres that snap back), 0 = low cohesion (deforms freely, stays gooey/merged).
/// </summary>
public const double DefaultFluidViscosity = 0.6;
/// <summary>
/// Default collision-strength dial. Mirrors <c>DEFAULT_COLLISION_STRENGTH</c> in MixVisualizer.ts.
/// Default collision-strength dial. Mirrors <c>DEFAULT_COLLISION_STRENGTH</c> in WaveformVisualizer.ts.
/// Normalized [0,1]; 0 = soft mush, 1 = hard elastic up-and-out throw.
/// </summary>
public const double DefaultCollisionStrength = 0.5;
/// <summary>
/// Default waveform-width dial. Mirrors <c>DEFAULT_WAVEFORM_WIDTH</c> in MixVisualizer.ts.
/// Default waveform-width dial. Mirrors <c>DEFAULT_WAVEFORM_WIDTH</c> in WaveformVisualizer.ts.
/// Normalized [0,1], mapped onto the useful 10%95% ribbon-extent band (Phase 10 §3.7); 0.5 opens
/// mid-band. Narrowing clears room for the lava.
/// </summary>
public const double DefaultWaveformWidth = 0.5;
/// <summary>Apparent bottom-to-top scroll rate, normalized [0,1]. Bridge maps it to a visible
/// time-span via <see cref="MixZoomMapping"/>; the standalone resolution/zoom control is gone.</summary>
/// time-span via <see cref="WaveformZoomMapping"/>; the standalone resolution/zoom control is gone.</summary>
public double ScrollSpeed { get; set; } = DefaultScrollSpeed;
/// <summary>Gradient anchor-rotation rate, normalized [0,1]. Drives the live OKLab gradient.</summary>
+3 -3
View File
@@ -26,9 +26,9 @@ public static class Startup
services.AddScoped<ReleaseDetailViewModel>();
services.AddScoped<CutDetailViewModel>();
// Mix visualizer controls — scoped so the four slider positions persist across navigation
// within a session and reset on a fresh page load (see MixVisualizerControlState).
services.AddScoped<MixVisualizerControlState>();
// Waveform visualizer controls — scoped so the eight slider positions persist across navigation
// within a session and reset on a fresh page load (see WaveformVisualizerControlState).
services.AddScoped<WaveformVisualizerControlState>();
}
public static void ConfigureApiHttpClient(IServiceCollection services, string baseAddress)