diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor index 3276f04..e33ff62 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor @@ -15,81 +15,92 @@ else
- @* Desktop Layout *@ -
-
-
- - @if (IsLoading && !IsStreaming) - { - - } + @if (_isDesktop) + { + @* Desktop Layout *@ +
+
+
+ + @if (IsLoading && !IsStreaming) + { + + } +
+
- -
-
- -
- -
- -
-
- - @* Mobile Layout *@ -
-
-
- - @if (IsLoading && !IsStreaming) - { - - } +
+
+ +
+
- - -
- -
- -
-
- @* Control Buttons - positioned absolutely like original *@ +
+ +
+
+ } + else + { + @* Mobile Layout *@ +
+
+
+ + @if (IsLoading && !IsStreaming) + { + + } +
+ + +
+ +
+
+ +
+ +
+
+ } + + @* Control Buttons - positioned absolutely like original *@
PlayerService.IsLoaded; private bool IsLoading => PlayerService.IsLoading; @@ -132,4 +136,31 @@ public partial class AudioPlayerBar : ComponentBase { return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var breakpoint = await BrowserViewportService.GetCurrentBreakpointAsync(); + _isDesktop = breakpoint >= Breakpoint.Sm; + + _viewportSubscriptionId = Guid.NewGuid(); + await BrowserViewportService.SubscribeAsync( + _viewportSubscriptionId, + args => + { + _isDesktop = args.Breakpoint >= Breakpoint.Sm; + InvokeAsync(StateHasChanged); + }, + new ResizeOptions { NotifyOnBreakpointOnly = true }, + fireImmediately: true); + + StateHasChanged(); + } + } + + public async ValueTask DisposeAsync() + { + await BrowserViewportService.UnsubscribeAsync(_viewportSubscriptionId); + } } \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css index c7da143..1401ebc 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css @@ -93,6 +93,12 @@ min-width: 200px; } +.seekbar-visualizer-container { + flex: 1; + display: flex; + flex-direction: column; +} + .seekbar-flex { flex: 1; } diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor b/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor new file mode 100644 index 0000000..55702bc --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor @@ -0,0 +1,12 @@ +@namespace DeepDrftWeb.Client.Controls.AudioPlayerBar + +
+
+ @for (int i = 0; i < BucketCount; i++) + { + var index = i; + var height = GetBarHeight(index); +
+ } +
+
diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs b/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs new file mode 100644 index 0000000..c804f9b --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs @@ -0,0 +1,114 @@ +using DeepDrftWeb.Client.Services; +using Microsoft.AspNetCore.Components; + +namespace DeepDrftWeb.Client.Controls.AudioPlayerBar; + +public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable +{ + [Inject] public required AudioInteropService AudioInterop { get; set; } + + [CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; } + + [Parameter] public int BucketCount { get; set; } = 32; + + private readonly string _instanceId = Guid.NewGuid().ToString(); + private double[] _spectrumData = Array.Empty(); + private bool _isAnimating = false; + private string? _playerId; + private EventCallback? _originalOnStateChanged; + + private bool IsVisible => PlayerService.IsPlaying || PlayerService.IsPaused || _isAnimating; + + protected override void OnInitialized() + { + _spectrumData = new double[BucketCount]; + + // Get the player ID from the service + if (PlayerService is AudioPlayerService baseService) + { + _playerId = baseService.PlayerId; + } + + // Chain into the existing OnStateChanged callback to detect play/pause + _originalOnStateChanged = PlayerService.OnStateChanged; + PlayerService.OnStateChanged = new EventCallback(this, async () => + { + // Call original callback first + if (_originalOnStateChanged.HasValue) + { + await _originalOnStateChanged.Value.InvokeAsync(); + } + // Then update our animation state + await UpdateAnimationState(); + }); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Initial check in case already playing + await UpdateAnimationState(); + } + } + + private async Task UpdateAnimationState() + { + if (string.IsNullOrEmpty(_playerId)) 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() + { + await StopAnimation(); + } +} diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.css b/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.css new file mode 100644 index 0000000..67696e9 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.css @@ -0,0 +1,48 @@ +.spectrum-container { + width: 100%; + height: 40px; + opacity: 1; + transition: opacity 0.3s ease; + overflow: hidden; +} + +.spectrum-container.hidden { + opacity: 0; +} + +.spectrum-bars { + display: flex; + justify-content: space-between; + align-items: flex-end; + height: 100%; + gap: 2px; + padding: 0 4px; +} + +.spectrum-bar { + flex: 1; + margin: 0 auto; + max-width: 8px; + min-width: 4px; + height: var(--bar-height, 2%); + min-height: 2px; + background: var(--deepdrft-theme-secondary, #8A2BE2); + border-radius: 2px 2px 0 0; + transition: height 0.05s ease-out; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .spectrum-container { + height: 32px; + } + + .spectrum-bars { + gap: 1px; + } + + .spectrum-bar { + max-width: 8px; + min-width: 3px; + } +} diff --git a/DeepDrftWeb.Client/Controls/TracksGallery.razor b/DeepDrftWeb.Client/Controls/TracksGallery.razor index b84311d..29e0a80 100644 --- a/DeepDrftWeb.Client/Controls/TracksGallery.razor +++ b/DeepDrftWeb.Client/Controls/TracksGallery.razor @@ -1,5 +1,5 @@  - + @foreach (var track in Tracks) { diff --git a/DeepDrftWeb.Client/Layout/MainLayout.razor b/DeepDrftWeb.Client/Layout/MainLayout.razor index f6cc905..4fef8eb 100644 --- a/DeepDrftWeb.Client/Layout/MainLayout.razor +++ b/DeepDrftWeb.Client/Layout/MainLayout.razor @@ -6,7 +6,7 @@ - + @@ -16,7 +16,7 @@ - + @Body diff --git a/DeepDrftWeb.Client/Layout/MainLayout.razor.css b/DeepDrftWeb.Client/Layout/MainLayout.razor.css new file mode 100644 index 0000000..60cec92 --- /dev/null +++ b/DeepDrftWeb.Client/Layout/MainLayout.razor.css @@ -0,0 +1,20 @@ +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/DeepDrftWeb.Client/Pages/TracksView.razor.css b/DeepDrftWeb.Client/Pages/TracksView.razor.css index 7b3c802..5215343 100644 --- a/DeepDrftWeb.Client/Pages/TracksView.razor.css +++ b/DeepDrftWeb.Client/Pages/TracksView.razor.css @@ -1,28 +1,23 @@ .tracks-page-wrapper { display: flex; flex-direction: column; - height: calc(100dvh - 80px); /* Subtract app bar height (pt-16 = 4rem = 64px) */ - /*margin: -16px; !* Counteract MudMainContent padding *!*/ - padding-top: 16px; /* Restore top padding for spacing */ } .tracks-view-container { display: flex; flex-direction: column; - flex: 1 1 auto; - min-height: 0; - overflow: hidden; + flex: 1; padding: 0 16px; /* Horizontal padding only */ } .tracks-content { - flex: 1 1 auto; - min-height: 0; + display: flex; + flex-grow: 1; padding-top: 16px; } .tracks-footer { - flex: 0 0 auto; + flex: 0 0; padding: 8px 0; display: flex; flex-direction: column; diff --git a/DeepDrftWeb.Client/Services/AudioInteropService.cs b/DeepDrftWeb.Client/Services/AudioInteropService.cs index 23a574c..148193d 100644 --- a/DeepDrftWeb.Client/Services/AudioInteropService.cs +++ b/DeepDrftWeb.Client/Services/AudioInteropService.cs @@ -5,7 +5,7 @@ namespace DeepDrftWeb.Client.Services; public class AudioInteropService : IAsyncDisposable { private readonly IJSRuntime _jsRuntime; - private readonly Dictionary> _callbacks = new(); + private readonly Dictionary _callbacks = new(); public AudioInteropService(IJSRuntime jsRuntime) { @@ -153,10 +153,66 @@ public class AudioInteropService : IAsyncDisposable public async Task SetOnEndCallbackAsync(string playerId, Func callback) { - return await SetCallbackAsync(playerId, "_end", "setOnEndCallback", "OnEndCallback", + return await SetCallbackAsync(playerId, "_end", "setOnEndCallback", "OnEndCallback", wrapper => wrapper.OnEnd = callback); } + // Spectrum analyzer methods + public async Task GetSpectrumDataAsync(string playerId) + { + try + { + return await _jsRuntime.InvokeAsync("DeepDrftAudio.getSpectrumData", playerId); + } + catch + { + return null; + } + } + + public async Task SetSpectrumHighPassAsync(string playerId, double freq) + { + return await InvokeJsAsync("DeepDrftAudio.setSpectrumHighPass", playerId, freq); + } + + public async Task SetSpectrumLowPassAsync(string playerId, double freq) + { + return await InvokeJsAsync("DeepDrftAudio.setSpectrumLowPass", playerId, freq); + } + + public async Task SetSpectrumSlopeAsync(string playerId, double dbPerDecade) + { + return await InvokeJsAsync("DeepDrftAudio.setSpectrumSlope", playerId, dbPerDecade); + } + + public async Task StartSpectrumAnimationAsync(string playerId, string callbackId, Func callback) + { + try + { + var callbackWrapper = new SpectrumCallback { OnData = callback }; + var dotNetObjectRef = DotNetObjectReference.Create(callbackWrapper); + _callbacks[playerId + "_spectrum_" + callbackId] = dotNetObjectRef; + + return await _jsRuntime.InvokeAsync( + "DeepDrftAudio.startSpectrumAnimation", + playerId, callbackId, dotNetObjectRef, "OnSpectrumDataCallback"); + } + catch (Exception ex) + { + return new AudioOperationResult { Success = false, Error = ex.Message }; + } + } + + public async Task StopSpectrumAnimationAsync(string playerId, string callbackId) + { + var key = playerId + "_spectrum_" + callbackId; + if (_callbacks.TryGetValue(key, out var callback)) + { + callback?.Dispose(); + _callbacks.Remove(key); + } + return await InvokeJsAsync("DeepDrftAudio.stopSpectrumAnimation", playerId, callbackId); + } public async Task DisposePlayerAsync(string playerId) { @@ -243,6 +299,18 @@ public class AudioPlayerCallback } } +public class SpectrumCallback +{ + public Func? OnData { get; set; } + + [JSInvokable] + public async Task OnSpectrumDataCallback(double[] data) + { + if (OnData != null) + await OnData(data); + } +} + public class AudioOperationResult { public bool Success { get; set; } diff --git a/DeepDrftWeb/Interop/audio/AudioContextManager.ts b/DeepDrftWeb/Interop/audio/AudioContextManager.ts index e45b6a4..e083a3d 100644 --- a/DeepDrftWeb/Interop/audio/AudioContextManager.ts +++ b/DeepDrftWeb/Interop/audio/AudioContextManager.ts @@ -2,10 +2,20 @@ * AudioContextManager - Manages the Web Audio API AudioContext and GainNode. * * Single Responsibility: AudioContext lifecycle and audio routing. + * + * Audio chain: Source → GainNode → AnalyserNode → destination */ + +import { SpectrumAnalyzer } from './SpectrumAnalyzer.js'; + export class AudioContextManager { private audioContext: AudioContext | null = null; private gainNode: GainNode | null = null; + private spectrumAnalyzer: SpectrumAnalyzer; + + constructor() { + this.spectrumAnalyzer = new SpectrumAnalyzer(); + } async initialize(sampleRate: number = 44100): Promise { const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext; @@ -15,7 +25,12 @@ export class AudioContextManager { this.audioContext = new AudioContextClass({ sampleRate }); this.gainNode = this.audioContext.createGain(); - this.gainNode.connect(this.audioContext.destination); + + // Initialize spectrum analyzer and insert into chain + // Chain: Source → GainNode → AnalyserNode → destination + const analyserNode = this.spectrumAnalyzer.initialize(this.audioContext); + this.gainNode.connect(analyserNode); + analyserNode.connect(this.audioContext.destination); console.log(`AudioContext initialized: sampleRate=${this.audioContext.sampleRate}Hz, state=${this.audioContext.state}`); } @@ -88,7 +103,12 @@ export class AudioContextManager { return this.audioContext.decodeAudioData(buffer); } + getSpectrumAnalyzer(): SpectrumAnalyzer { + return this.spectrumAnalyzer; + } + dispose(): void { + this.spectrumAnalyzer.dispose(); if (this.audioContext && this.audioContext.state !== 'closed') { this.audioContext.close(); } diff --git a/DeepDrftWeb/Interop/audio/AudioPlayer.ts b/DeepDrftWeb/Interop/audio/AudioPlayer.ts index 6c3d02e..b4e937a 100644 --- a/DeepDrftWeb/Interop/audio/AudioPlayer.ts +++ b/DeepDrftWeb/Interop/audio/AudioPlayer.ts @@ -389,6 +389,47 @@ export class AudioPlayer { this.onEndCallback = callback; } + // ==================== Spectrum Analysis ==================== + + getSpectrumData(): number[] { + return this.contextManager.getSpectrumAnalyzer().getFrequencyData(); + } + + setSpectrumHighPass(freq: number): AudioResult { + try { + this.contextManager.getSpectrumAnalyzer().setHighPass(freq); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + } + + setSpectrumLowPass(freq: number): AudioResult { + try { + this.contextManager.getSpectrumAnalyzer().setLowPass(freq); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + } + + setSpectrumSlope(dbPerDecade: number): AudioResult { + try { + this.contextManager.getSpectrumAnalyzer().setSlopeCorrection(dbPerDecade); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + } + + startSpectrumAnimation(callbackId: string, callback: (data: number[]) => void): void { + this.contextManager.getSpectrumAnalyzer().addCallback(callbackId, callback); + } + + stopSpectrumAnimation(callbackId: string): void { + this.contextManager.getSpectrumAnalyzer().removeCallback(callbackId); + } + // ==================== Private Methods ==================== private resetState(): void { diff --git a/DeepDrftWeb/Interop/audio/SpectrumAnalyzer.ts b/DeepDrftWeb/Interop/audio/SpectrumAnalyzer.ts new file mode 100644 index 0000000..3314502 --- /dev/null +++ b/DeepDrftWeb/Interop/audio/SpectrumAnalyzer.ts @@ -0,0 +1,213 @@ +/** + * SpectrumAnalyzer - Manages FFT analysis with filtering and slope correction. + * + * Single Responsibility: FFT analysis, frequency bucketing, and visual processing filters. + */ + +export interface SpectrumConfig { + bucketCount: number; + highPassFreq: number; // Hz, 0 = disabled + lowPassFreq: number; // Hz + slopeDb: number; // dB/decade correction +} + +export class SpectrumAnalyzer { + private analyser: AnalyserNode | null = null; + private audioContext: AudioContext | null = null; + private fftSize: number = 2048; + private dataArray: Float32Array | null = null; + + // Configuration + private bucketCount: number = 32; + private highPassFreq: number = 0; + private lowPassFreq: number = 20000; + private slopeDb: number = 0; + + // Animation state - supports multiple callbacks per player + private animationId: number | null = null; + private callbacks = new Map void>(); + private lastFrameTime: number = 0; + private targetFrameInterval: number = 1000 / 30; // ~30fps for smooth visuals without excessive interop + + initialize(context: AudioContext): AnalyserNode { + this.audioContext = context; + this.analyser = context.createAnalyser(); + this.analyser.fftSize = this.fftSize; + this.analyser.smoothingTimeConstant = 0.8; + this.dataArray = new Float32Array(this.analyser.frequencyBinCount); + + console.log(`SpectrumAnalyzer initialized: fftSize=${this.fftSize}, bins=${this.analyser.frequencyBinCount}`); + return this.analyser; + } + + getAnalyserNode(): AnalyserNode | null { + return this.analyser; + } + + setConfig(config: Partial): void { + if (config.bucketCount !== undefined) this.bucketCount = config.bucketCount; + if (config.highPassFreq !== undefined) this.highPassFreq = config.highPassFreq; + if (config.lowPassFreq !== undefined) this.lowPassFreq = config.lowPassFreq; + if (config.slopeDb !== undefined) this.slopeDb = config.slopeDb; + } + + setHighPass(freq: number): void { + this.highPassFreq = Math.max(0, freq); + } + + setLowPass(freq: number): void { + this.lowPassFreq = Math.max(20, freq); + } + + setSlopeCorrection(dbPerDecade: number): void { + this.slopeDb = dbPerDecade; + } + + /** + * Get frequency data as normalized values (0-1) for each bucket + */ + getFrequencyData(): number[] { + if (!this.analyser || !this.dataArray || !this.audioContext) { + return new Array(this.bucketCount).fill(0); + } + + // Get raw FFT data (in dB, typically -100 to 0) + this.analyser.getFloatFrequencyData(this.dataArray); + + const nyquist = this.audioContext.sampleRate / 2; + const binCount = this.dataArray.length; + const buckets: number[] = new Array(this.bucketCount).fill(0); + + // Logarithmic frequency mapping for perceptual balance + // Map 20Hz - 20kHz to buckets using log scale + const minFreq = 20; + const maxFreq = Math.min(20000, nyquist); + const logMin = Math.log10(minFreq); + const logMax = Math.log10(maxFreq); + const logRange = logMax - logMin; + + for (let bucket = 0; bucket < this.bucketCount; bucket++) { + // Calculate frequency range for this bucket + const logFreqLow = logMin + (bucket / this.bucketCount) * logRange; + const logFreqHigh = logMin + ((bucket + 1) / this.bucketCount) * logRange; + const freqLow = Math.pow(10, logFreqLow); + const freqHigh = Math.pow(10, logFreqHigh); + + // Map frequencies to FFT bins + const binLow = Math.floor((freqLow / nyquist) * binCount); + const binHigh = Math.ceil((freqHigh / nyquist) * binCount); + + // Average the bins in this range + let sum = 0; + let count = 0; + for (let bin = binLow; bin < binHigh && bin < binCount; bin++) { + const freq = (bin / binCount) * nyquist; + let value = this.dataArray[bin]; + + // Apply filters + value = this.applyFilters(value, freq); + + sum += value; + count++; + } + + const avgDb = count > 0 ? sum / count : -100; + + // Normalize from dB (-100 to 0) to 0-1 range + // Clamp to reasonable range and scale + const normalizedDb = Math.max(-80, Math.min(0, avgDb)); + buckets[bucket] = (normalizedDb + 80) / 80; + } + + return buckets; + } + + /** + * Apply high-pass, low-pass, and slope correction filters + */ + private applyFilters(valueDb: number, freq: number): number { + // Convert dB to linear for filter math + let linear = Math.pow(10, valueDb / 20); + + // High-pass filter (6dB/octave) + if (this.highPassFreq > 0 && freq < this.highPassFreq && freq > 0) { + const octaves = Math.log2(this.highPassFreq / freq); + const attenuation = Math.pow(10, (-6 * octaves) / 20); + linear *= attenuation; + } + + // Low-pass filter (6dB/octave) + if (freq > this.lowPassFreq && this.lowPassFreq > 0) { + const octaves = Math.log2(freq / this.lowPassFreq); + const attenuation = Math.pow(10, (-6 * octaves) / 20); + linear *= attenuation; + } + + // Slope correction (dB/decade, referenced to 1kHz) + if (this.slopeDb !== 0 && freq > 0) { + const decades = Math.log10(freq / 1000); + const correction = Math.pow(10, (this.slopeDb * decades) / 20); + linear *= correction; + } + + // Convert back to dB + return linear > 0 ? 20 * Math.log10(linear) : -100; + } + + /** + * Add a callback for spectrum data. Starts animation loop on first subscriber. + */ + addCallback(id: string, callback: (data: number[]) => void): void { + const wasEmpty = this.callbacks.size === 0; + this.callbacks.set(id, callback); + if (wasEmpty) { + this.lastFrameTime = 0; + this.animationId = requestAnimationFrame(this.animate); + } + } + + /** + * Remove a callback by ID. Stops animation loop when no subscribers remain. + */ + removeCallback(id: string): void { + this.callbacks.delete(id); + if (this.callbacks.size === 0) { + this.stopAnimation(); + } + } + + /** + * Stop animation loop + */ + stopAnimation(): void { + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId); + this.animationId = null; + } + } + + private animate = (timestamp: number): void => { + if (this.callbacks.size === 0) return; + + // Throttle to target frame rate + const elapsed = timestamp - this.lastFrameTime; + if (elapsed >= this.targetFrameInterval) { + this.lastFrameTime = timestamp - (elapsed % this.targetFrameInterval); + const data = this.getFrequencyData(); + // Broadcast to all callbacks + for (const cb of this.callbacks.values()) { + cb(data); + } + } + + this.animationId = requestAnimationFrame(this.animate); + }; + + dispose(): void { + this.stopAnimation(); + this.callbacks.clear(); + this.analyser = null; + this.audioContext = null; + this.dataArray = null; + } +} diff --git a/DeepDrftWeb/Interop/audio/index.ts b/DeepDrftWeb/Interop/audio/index.ts index 69d41b5..164dcc2 100644 --- a/DeepDrftWeb/Interop/audio/index.ts +++ b/DeepDrftWeb/Interop/audio/index.ts @@ -142,6 +142,52 @@ const DeepDrftAudio = { return { success: true }; }, + // Spectrum analyzer methods + getSpectrumData: (playerId: string): number[] | null => { + const player = audioPlayers.get(playerId); + return player?.getSpectrumData() ?? null; + }, + + setSpectrumHighPass: (playerId: string, freq: number): AudioResult => { + const player = audioPlayers.get(playerId); + if (!player) return { success: false, error: 'Player not found' }; + return player.setSpectrumHighPass(freq); + }, + + setSpectrumLowPass: (playerId: string, freq: number): AudioResult => { + const player = audioPlayers.get(playerId); + if (!player) return { success: false, error: 'Player not found' }; + return player.setSpectrumLowPass(freq); + }, + + setSpectrumSlope: (playerId: string, dbPerDecade: number): AudioResult => { + const player = audioPlayers.get(playerId); + if (!player) return { success: false, error: 'Player not found' }; + return player.setSpectrumSlope(dbPerDecade); + }, + + startSpectrumAnimation: ( + playerId: string, + callbackId: string, + dotNetRef: DotNetObjectReference, + methodName: string + ): AudioResult => { + const player = audioPlayers.get(playerId); + if (!player) return { success: false, error: 'Player not found' }; + + player.startSpectrumAnimation(callbackId, (data: number[]) => { + dotNetRef.invokeMethodAsync(methodName, data); + }); + return { success: true }; + }, + + stopSpectrumAnimation: (playerId: string, callbackId: string): AudioResult => { + const player = audioPlayers.get(playerId); + if (!player) return { success: false, error: 'Player not found' }; + player.stopSpectrumAnimation(callbackId); + return { success: true }; + }, + disposePlayer: (playerId: string): AudioResult => { const player = audioPlayers.get(playerId); if (player) { diff --git a/DeepDrftWeb/wwwroot/styles/deepdrft-styles.css b/DeepDrftWeb/wwwroot/styles/deepdrft-styles.css index 685156e..8bd4de9 100644 --- a/DeepDrftWeb/wwwroot/styles/deepdrft-styles.css +++ b/DeepDrftWeb/wwwroot/styles/deepdrft-styles.css @@ -294,29 +294,6 @@ body, p, span, div, justify-content: center; } - -/* Layout with overlay audio player - Global layout class */ -/*.deepdrft-layout-with-overlay-player {*/ -/* position: relative;*/ -/* min-height: calc(100vh - 64px);*/ -/* padding-bottom: 160px; !* Increased space for overlay player *!*/ -/*}*/ - -/*!* Audio player overlay positioning - Global positioning *!*/ -/*.deepdrft-layout-with-overlay-player > .AudioPlayerBar,*/ -/*.deepdrft-layout-with-overlay-player > *:last-child {*/ -/* position: fixed;*/ -/* bottom: 0;*/ -/* left: 0;*/ -/* right: 0;*/ -/* z-index: 1000;*/ -/* pointer-events: none;*/ -/*}*/ - -/*.deepdrft-layout-with-overlay-player > *:last-child > * {*/ -/* pointer-events: auto;*/ -/*}*/ - /* Responsive Utilities */ @media (max-width: 768px) { .deepdrft-hero-text {