Files
daniel-c-harvey cc1fa60a4d refactor(player): move SpectrumVisualizer into VolumeZone above volume slider
Rename VolumeControls to VolumeZone; stack 24-bucket SpectrumVisualizer above volume
slider; remove it from PlayerSeekZone. MudSlider stays as seek placeholder. Pin
flex-shrink:0 on volume-zone; add Class param to VolumeZone for layout flexibility.
2026-06-05 16:38:13 -04:00

125 lines
3.8 KiB
C#

using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
{
[Inject] public required AudioInteropService AudioInterop { get; set; }
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
[Parameter] public int BucketCount { get; set; } = 24;
private readonly string _instanceId = Guid.NewGuid().ToString();
private double[] _spectrumData = Array.Empty<double>();
private bool _isAnimating = false;
private string? _playerId;
private IStreamingPlayerService? _subscribedService;
private bool IsVisible => (PlayerService?.IsPlaying ?? false) || (PlayerService?.IsPaused ?? false) || _isAnimating;
protected override void OnInitialized()
{
_spectrumData = new double[BucketCount];
}
protected override async Task OnParametersSetAsync()
{
// The cascade is IsFixed, so the provider's re-renders do NOT re-run
// OnParametersSet here, and this component has no incoming parameters
// that change. Subscribe to the multicast StateChanged side-channel so
// animation state stays correct independent of parent re-renders —
// notably when the bar expands while a track is already playing.
if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService))
{
if (_subscribedService != null)
_subscribedService.StateChanged -= OnPlayerStateChanged;
PlayerService.StateChanged += OnPlayerStateChanged;
_subscribedService = PlayerService;
}
if (_playerId == null && PlayerService is AudioPlayerService baseService)
{
_playerId = baseService.PlayerId;
}
await UpdateAnimationState();
}
private void OnPlayerStateChanged() => InvokeAsync(async () =>
{
if (_playerId == null && PlayerService is AudioPlayerService baseService)
{
_playerId = baseService.PlayerId;
}
await UpdateAnimationState();
});
private async Task UpdateAnimationState()
{
if (string.IsNullOrEmpty(_playerId) || PlayerService == null) return;
var shouldAnimate = PlayerService.IsPlaying;
if (shouldAnimate && !_isAnimating)
{
await StartAnimation();
}
else if (!shouldAnimate && _isAnimating)
{
await StopAnimation();
}
}
private async Task StartAnimation()
{
if (_isAnimating || string.IsNullOrEmpty(_playerId)) return;
_isAnimating = true;
await AudioInterop.StartSpectrumAnimationAsync(_playerId, _instanceId, OnSpectrumData);
}
private async Task StopAnimation()
{
if (!_isAnimating || string.IsNullOrEmpty(_playerId)) return;
_isAnimating = false;
await AudioInterop.StopSpectrumAnimationAsync(_playerId, _instanceId);
// Clear the display
Array.Clear(_spectrumData);
await InvokeAsync(StateHasChanged);
}
private Task OnSpectrumData(double[] data)
{
if (data.Length > 0)
{
_spectrumData = data;
InvokeAsync(StateHasChanged);
}
return Task.CompletedTask;
}
private double GetBarHeight(int index)
{
if (index >= _spectrumData.Length) return 0;
// Scale to 0-100 percentage, with minimum height for visual appeal
var value = _spectrumData[index];
return Math.Max(2, value * 100);
}
public async ValueTask DisposeAsync()
{
if (_subscribedService != null)
{
_subscribedService.StateChanged -= OnPlayerStateChanged;
_subscribedService = null;
}
await StopAnimation();
}
}