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(); 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(); } }