fix(visualizer): lay Mix datum across a 2-D R8 texture to respect GL_MAX_TEXTURE_SIZE; manual texelFetch lerp avoids row-wrap seam
This commit is contained in:
@@ -54,6 +54,18 @@ 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
|
||||
// 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
|
||||
// which upstream link is broken when the ribbon stays blank — set false once confirmed healthy.
|
||||
private const bool Debug = true;
|
||||
private const string Tag = "[MixVisualizer]";
|
||||
|
||||
private static void DebugLog(string message)
|
||||
{
|
||||
if (Debug) Console.WriteLine($"{Tag} {message}");
|
||||
}
|
||||
|
||||
private ElementReference _canvas;
|
||||
private IJSObjectReference? _module;
|
||||
private IJSObjectReference? _handle;
|
||||
@@ -81,12 +93,19 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// Subscribe to the player's multicast side-channel once, to re-render on position/play ticks.
|
||||
// Log whether the cascade is even present: a null PlayerService here means the visualizer
|
||||
// never couples to playback (no StateChanged events ever reach OnPlayerStateChanged).
|
||||
if (PlayerService is not null && !ReferenceEquals(PlayerService, _subscribedService))
|
||||
{
|
||||
if (_subscribedService is not null)
|
||||
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
||||
PlayerService.StateChanged += OnPlayerStateChanged;
|
||||
_subscribedService = PlayerService;
|
||||
DebugLog($"subscribed to player StateChanged. ReleaseId={ReleaseId}, TrackId={TrackId?.ToString() ?? "null"}.");
|
||||
}
|
||||
else if (PlayerService is null)
|
||||
{
|
||||
DebugLog($"NO player cascade — playback will never couple. ReleaseId={ReleaseId}, TrackId={TrackId?.ToString() ?? "null"}.");
|
||||
}
|
||||
|
||||
// ReleaseId is the only fetch input; fetch once per id. Position/zoom/theme changes re-render
|
||||
@@ -95,11 +114,13 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
if (_loadedReleaseId == ReleaseId) return;
|
||||
_loadedReleaseId = ReleaseId;
|
||||
|
||||
DebugLog($"fetching mix waveform datum for ReleaseId={ReleaseId}…");
|
||||
var result = await ReleaseData.GetMixWaveform(ReleaseId);
|
||||
if (result is { Success: true, Value: { } profile } && profile.BucketCount > 0 && profile.Data.Length > 0)
|
||||
{
|
||||
_profile = profile;
|
||||
_hasDatum = true;
|
||||
DebugLog($"datum fetch OK — {profile.BucketCount} buckets, base64 length {profile.Data.Length}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -107,6 +128,9 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
// renders its content over a plain background.
|
||||
_profile = null;
|
||||
_hasDatum = false;
|
||||
DebugLog(result.Success
|
||||
? $"datum fetch returned EMPTY/absent (no stored datum for ReleaseId={ReleaseId}) — backdrop stays blank."
|
||||
: $"datum fetch FAILED ({result.GetMessage() ?? "unknown error"}) — backdrop stays blank.");
|
||||
}
|
||||
|
||||
// Push the (possibly new) datum to the module if it is already created.
|
||||
@@ -117,6 +141,10 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
{
|
||||
// Position/play-state changed: push it to the module (cheap; no re-fetch, no full re-render
|
||||
// needed for the canvas itself, but StateHasChanged keeps the slider/visibility in sync).
|
||||
// Log the gating inputs so a "ribbon never couples" failure shows exactly why: whether the
|
||||
// player is on THIS track (IsActivePlayer), and what duration/position/play-state it reports.
|
||||
var currentTrackId = PlayerService?.CurrentTrack is { } ct ? ct.Id.ToString() : "null";
|
||||
DebugLog($"player StateChanged — IsActivePlayer={IsActivePlayer} (player.CurrentTrack.Id={currentTrackId}, TrackId={TrackId?.ToString() ?? "null"}), player.IsPlaying={PlayerService?.IsPlaying}, player.Duration={PlayerService?.Duration?.ToString("F2") ?? "null"}.");
|
||||
await PushPlaybackAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
@@ -173,11 +201,20 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
if (ReferenceEquals(_profile, _pushedProfile) && haveDuration == _pushedWithDuration)
|
||||
return;
|
||||
|
||||
if (!haveDuration)
|
||||
{
|
||||
// The most common stuck state: a datum is loaded but no positive player duration has
|
||||
// arrived, so we cannot map samples↔time and push an empty datum. Spell out which half
|
||||
// is missing so the broken link is unambiguous in the console.
|
||||
DebugLog($"datum push deferred (empty) — profile={(_profile is null ? "null" : "loaded")}, playerDuration={PlayerDurationSeconds?.ToString("F2") ?? "null"} (needs IsActivePlayer + duration>0).");
|
||||
}
|
||||
|
||||
if (haveDuration)
|
||||
{
|
||||
// The mix duration must come from the player (no DTO field carries it); without a
|
||||
// positive duration we cannot map samples↔time, so we hold off until it arrives.
|
||||
await _handle.InvokeVoidAsync("setDatum", _profile!.Data, PlayerDurationSeconds!.Value);
|
||||
DebugLog($"datum push (REAL) — base64 length {_profile!.Data.Length}, duration {PlayerDurationSeconds!.Value:F2}s.");
|
||||
await _handle.InvokeVoidAsync("setDatum", _profile.Data, PlayerDurationSeconds.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -190,12 +227,17 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
|
||||
private async Task PushPlaybackAsync()
|
||||
{
|
||||
if (_handle is null) return;
|
||||
if (_handle is null)
|
||||
{
|
||||
DebugLog("PushPlayback skipped — module handle not created yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Duration arrives via the player after the initial (duration-less) datum push; the
|
||||
// idempotent PushDatumAsync re-pushes exactly once when it first becomes available.
|
||||
await PushDatumAsync();
|
||||
|
||||
DebugLog($"setPlayback → position={CurrentPositionSeconds:F2}s, isPlaying={IsPlaying}.");
|
||||
await _handle.InvokeVoidAsync("setPlayback", CurrentPositionSeconds, IsPlaying);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user