fix(visualizer): follow the live playing track, not the fixed host TrackId
Replace the TrackId-only IsActivePlayer gate with a LivePlayerTrack source that follows the playing track when it is the host track or shares the host release; single-track Mix/Session unchanged at parity.
This commit is contained in:
@@ -10,10 +10,11 @@ namespace DeepDrftPublic.Client.Controls;
|
||||
/// <summary>
|
||||
/// Scrolling waveform visualizer, track-cardinal (phase-12 §4/§5). It renders the high-res loudness
|
||||
/// datum of whatever track is currently playing/selected: the datum is the track's, not the release's,
|
||||
/// so the fetch resolves the current track's <c>EntryKey</c> (the playing track when this is the active
|
||||
/// player, else the host-supplied <see cref="TrackEntryKey"/>) and re-fetches when that track identity
|
||||
/// changes — not when the release changes. The release (<see cref="ReleaseEntryKey"/>) is only addressing
|
||||
/// context. The rendering itself — a windowed, bottom-to-top, playback-coupled scroll with a glassy
|
||||
/// so the fetch resolves the current track's <c>EntryKey</c> (the live playing track when the cascaded
|
||||
/// player is this visualizer's source — see <see cref="LivePlayerTrack"/> — else the host-supplied
|
||||
/// <see cref="TrackEntryKey"/>) and re-fetches when that track identity changes — not when the release
|
||||
/// changes. The release (<see cref="ReleaseEntryKey"/>) anchors which playing tracks this visualizer
|
||||
/// follows (its own and any sibling in the same release), and is otherwise addressing context. The rendering itself — a windowed, bottom-to-top, playback-coupled scroll with a glassy
|
||||
/// theme-aware gradient — lives in the WaveformVisualizer.ts interop module; this component is the bridge
|
||||
/// that feeds it datum, playback position, zoom, and theme, and owns the module lifecycle.
|
||||
///
|
||||
@@ -47,19 +48,21 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
[Parameter] public required string ReleaseEntryKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of the host's selected/default playable track. Used to gate the cascaded player as the
|
||||
/// live source: we only couple to playback when the player is on THIS track, so a different track
|
||||
/// playing elsewhere leaves this visualizer at its at-rest slice instead of scrolling to the wrong
|
||||
/// audio. Null leaves the visualizer in the at-rest state (no player coupling).
|
||||
/// The id of the host's selected/default playable track. Anchors the host's at-rest identity: at
|
||||
/// rest (no live player) the visualizer renders this track's datum via <see cref="TrackEntryKey"/>.
|
||||
/// It also keeps an unrelated track playing elsewhere from coupling to this visualizer — the live
|
||||
/// player only becomes this visualizer's source when its track is part of this host (it IS this
|
||||
/// track, or it shares this host's <see cref="ReleaseEntryKey"/>; see <see cref="LivePlayerTrack"/>).
|
||||
/// Null leaves the visualizer in the at-rest state unless the player's track matches the release.
|
||||
/// </summary>
|
||||
[Parameter] public long? TrackId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The EntryKey of the host's selected/default track — the datum to render when no matching player
|
||||
/// is active (e.g. a Mix detail page at rest, before playback starts). When the cascaded player is on
|
||||
/// this visualizer's track (<see cref="IsActivePlayer"/>), the playing track's EntryKey takes
|
||||
/// precedence so the datum follows live playback (a multi-track Cut, the NowPlaying card). Null with
|
||||
/// no active player leaves the visualizer blank.
|
||||
/// The EntryKey of the host's selected/default track — the datum to render when no live player is
|
||||
/// this visualizer's source (e.g. a Mix detail page at rest, before playback starts). When the
|
||||
/// cascaded player is this visualizer's live source (<see cref="LivePlayerTrack"/>), the playing
|
||||
/// track's own EntryKey takes precedence so the datum follows live playback (a multi-track Cut, the
|
||||
/// NowPlaying card). Null with no live player leaves the visualizer blank.
|
||||
/// </summary>
|
||||
[Parameter] public string? TrackEntryKey { get; set; }
|
||||
|
||||
@@ -146,12 +149,17 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The EntryKey of the track whose datum to render: the live playing track when this visualizer is
|
||||
/// the active player, otherwise the host's selected/default <see cref="TrackEntryKey"/>. This is the
|
||||
/// single source of "which track's datum" — both the fetch key and what re-arms the fetch-once guard.
|
||||
/// The EntryKey of the track whose datum to render: the live playing track's own EntryKey when the
|
||||
/// cascaded player is this visualizer's source (<see cref="LivePlayerTrack"/>), otherwise the host's
|
||||
/// selected/default <see cref="TrackEntryKey"/>. This is the single source of "which track's datum" —
|
||||
/// both the fetch key and what re-arms the fetch-once guard. Following the *live* track (not the fixed
|
||||
/// host <see cref="TrackId"/>) is what lets a multi-track Cut, or the NowPlaying card, re-fetch and
|
||||
/// render the now-playing track as playback advances (phase-12 §4/§6c). At rest it degrades to the
|
||||
/// host track — single-track hosts (Mix/Session), where the live track IS the host track, are
|
||||
/// unchanged.
|
||||
/// </summary>
|
||||
private string? CurrentTrackKey =>
|
||||
IsActivePlayer ? PlayerService!.CurrentTrack!.EntryKey : TrackEntryKey;
|
||||
LivePlayerTrack?.EntryKey ?? TrackEntryKey;
|
||||
|
||||
/// <summary>
|
||||
/// Fetch the current track's high-res datum, but only when the track identity changed since the last
|
||||
@@ -217,10 +225,11 @@ public partial class WaveformVisualizer : 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.
|
||||
// Log the gating inputs so a "ribbon never couples" failure shows exactly why: whether the live
|
||||
// player is this visualizer's source (LivePlayerTrack), 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"}.");
|
||||
DebugLog($"player StateChanged — liveTrackKey={LivePlayerTrack?.EntryKey ?? "null"} (player.CurrentTrack.Id={currentTrackId}, TrackId={TrackId?.ToString() ?? "null"}, ReleaseEntryKey={ReleaseEntryKey}), player.IsPlaying={PlayerService?.IsPlaying}, player.Duration={PlayerService?.Duration?.ToString("F2") ?? "null"}.");
|
||||
// The playing track may have changed (a multi-track Cut advancing, the NowPlaying card following
|
||||
// playback) — re-fetch its datum if so. EnsureDatumForCurrentTrackAsync is guarded, so a tick that
|
||||
// didn't change the track is a cheap no-op.
|
||||
@@ -321,7 +330,7 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
// 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).");
|
||||
DebugLog($"datum push deferred (empty) — profile={(_profile is null ? "null" : "loaded")}, playerDuration={PlayerDurationSeconds?.ToString("F2") ?? "null"} (needs a live player source + duration>0).");
|
||||
}
|
||||
|
||||
if (haveDuration)
|
||||
@@ -369,27 +378,50 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
await _handle.InvokeVoidAsync("refreshTheme");
|
||||
}
|
||||
|
||||
// ── Live signal sources. The matching player wins; PlaybackPosition is the no-player fallback. ─
|
||||
// ── Live signal sources. The live player track wins; PlaybackPosition is the no-player fallback. ─
|
||||
|
||||
/// <summary>True only when the cascaded player is loaded with THIS mix's track.</summary>
|
||||
private bool IsActivePlayer =>
|
||||
PlayerService is { CurrentTrack: not null }
|
||||
&& TrackId is { } id
|
||||
&& PlayerService.CurrentTrack.Id == id;
|
||||
/// <summary>
|
||||
/// The cascaded player's current track when that player is *this visualizer's* live source, else null.
|
||||
/// A player track is this visualizer's source when it is part of what this host represents:
|
||||
/// <list type="bullet">
|
||||
/// <item>it IS the host's pinned track (<c>CurrentTrack.Id == TrackId</c>) — the single-track
|
||||
/// Mix/Session case, preserved at exact parity; or</item>
|
||||
/// <item>it shares the host's release (<c>CurrentTrack.Release.EntryKey == ReleaseEntryKey</c>) —
|
||||
/// a multi-track Cut where the release is fixed but the playing track scrolls.</item>
|
||||
/// </list>
|
||||
/// The release-match disjunct is what lets the visualizer FOLLOW the live track as playback advances
|
||||
/// past the host's fixed <see cref="TrackId"/> (phase-12 §4) instead of reverting to the host default;
|
||||
/// the id-match disjunct guarantees single-track hosts behave identically even if the player track's
|
||||
/// release graph is sparse. A track playing that is neither this track nor in this release is NOT our
|
||||
/// source — the visualizer stays at the host's at-rest slice rather than scrolling to unrelated audio.
|
||||
/// This is the single gate every live-signal accessor (datum key, duration, position, play-state)
|
||||
/// shares, so they cannot disagree about which track is live.
|
||||
/// </summary>
|
||||
private TrackDto? LivePlayerTrack
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlayerService?.CurrentTrack is not { } track) return null;
|
||||
if (TrackId is { } id && track.Id == id) return track;
|
||||
if (!string.IsNullOrEmpty(track.Release?.EntryKey)
|
||||
&& track.Release.EntryKey == ReleaseEntryKey) return track;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private double? PlayerDurationSeconds =>
|
||||
IsActivePlayer && PlayerService!.Duration is > 0 ? PlayerService.Duration : null;
|
||||
LivePlayerTrack is not null && PlayerService!.Duration is > 0 ? PlayerService.Duration : null;
|
||||
|
||||
private bool IsPlaying => IsActivePlayer && (PlayerService?.IsPlaying ?? false);
|
||||
private bool IsPlaying => LivePlayerTrack is not null && (PlayerService?.IsPlaying ?? false);
|
||||
|
||||
private double CurrentPositionSeconds
|
||||
{
|
||||
get
|
||||
{
|
||||
// Prefer the matching player's absolute time. Otherwise fall back to the one-way
|
||||
// Prefer the live player's absolute time. Otherwise fall back to the one-way
|
||||
// PlaybackPosition ([0,1]) scaled by whatever duration we have; with no duration the
|
||||
// position is unusable, so show the at-rest slice (0).
|
||||
if (IsActivePlayer)
|
||||
if (LivePlayerTrack is not null)
|
||||
return PlayerService!.CurrentTime;
|
||||
if (PlayerDurationSeconds is { } dur)
|
||||
return Math.Clamp(PlaybackPosition, 0, 1) * dur;
|
||||
|
||||
Reference in New Issue
Block a user