diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor index 85ddd00..2660b15 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor @@ -2,8 +2,8 @@ {
- diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs index 5cf4c85..c8dc0be 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs @@ -15,6 +15,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable private double _seekPosition = 0; private bool _isDesktop = true; private Guid _viewportSubscriptionId; + private IStreamingPlayerService? _subscribedService; private bool IsLoaded => PlayerService?.IsLoaded ?? false; private bool IsLoading => PlayerService?.IsLoading ?? false; @@ -42,14 +43,23 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable { // PlayerService is cascaded by AudioPlayerProvider; once it arrives, // wire our track-selection handler. The provider owns OnStateChanged — - // we intentionally do NOT wrap or replace it. Re-renders propagate - // from the provider via the standard Blazor child render path. - if (PlayerService != null) + // we intentionally do NOT wrap or replace it. Because the cascade is + // IsFixed, the provider's re-render does NOT reliably re-render this bar + // (it has no incoming parameters that change), so we subscribe to the + // multicast StateChanged side-channel to re-render ourselves. + if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService)) { + if (_subscribedService != null) + _subscribedService.StateChanged -= OnPlayerStateChanged; + PlayerService.OnTrackSelected = new EventCallback(this, Expand); + PlayerService.StateChanged += OnPlayerStateChanged; + _subscribedService = PlayerService; } } + private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged); + private async Task Expand() { if (_isMinimized) @@ -58,11 +68,6 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable StateHasChanged(); } } - private static string FormatTime(double seconds) - { - var timeSpan = TimeSpan.FromSeconds(seconds); - return timeSpan.ToString(timeSpan.TotalHours >= 1 ? @"h\:mm\:ss" : @"m\:ss"); - } private async Task TogglePlayPause() { @@ -127,11 +132,6 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable } } - private string GetPlayIcon() - { - return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow; - } - protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -156,6 +156,11 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable public async ValueTask DisposeAsync() { + if (_subscribedService != null) + { + _subscribedService.StateChanged -= OnPlayerStateChanged; + _subscribedService = null; + } await BrowserViewportService.UnsubscribeAsync(_viewportSubscriptionId); } } \ No newline at end of file diff --git a/DeepDrftPublic.Client/Pages/TracksView.razor.cs b/DeepDrftPublic.Client/Pages/TracksView.razor.cs index 316e807..71268a5 100644 --- a/DeepDrftPublic.Client/Pages/TracksView.razor.cs +++ b/DeepDrftPublic.Client/Pages/TracksView.razor.cs @@ -6,21 +6,51 @@ using Models.Common; namespace DeepDrftPublic.Client.Pages; -public partial class TracksView : ComponentBase +public partial class TracksView : ComponentBase, IDisposable { [Inject] public required TracksViewModel ViewModel { get; set; } [CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; } - + private TrackDto? _selectedTrack = null; private int _clickCount = 0; private string _lifecycleStatus = "Not initialized"; - + private IStreamingPlayerService? _subscribedService; + protected override async Task OnInitializedAsync() { _lifecycleStatus = "OnInitializedAsync called"; await SetPage(1); } + protected override void OnParametersSet() + { + // The Stop/Close buttons on the player bar reset the player directly, + // bypassing PlayTrack — so the gallery's selection must follow player + // state rather than only its own clicks. Subscribe to the multicast + // side-channel (the cascade is IsFixed, so provider re-renders don't + // reach us) and clear the highlight when nothing is loaded. + if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService)) + { + if (_subscribedService != null) + _subscribedService.StateChanged -= OnPlayerStateChanged; + + PlayerService.StateChanged += OnPlayerStateChanged; + _subscribedService = PlayerService; + } + } + + private void OnPlayerStateChanged() + { + // Sync the gallery selection to the player. When the player is no longer + // loaded (stopped/closed/ended) drop the highlight; guard against a + // redundant re-render when nothing actually changed. + if (!PlayerService.IsLoaded && _selectedTrack != null) + { + _selectedTrack = null; + InvokeAsync(StateHasChanged); + } + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -62,4 +92,13 @@ public partial class TracksView : ComponentBase _selectedTrack = track; } + + public void Dispose() + { + if (_subscribedService != null) + { + _subscribedService.StateChanged -= OnPlayerStateChanged; + _subscribedService = null; + } + } } \ No newline at end of file diff --git a/DeepDrftPublic.Client/Services/AudioInteropService.cs b/DeepDrftPublic.Client/Services/AudioInteropService.cs index 91ecb8b..91b6922 100644 --- a/DeepDrftPublic.Client/Services/AudioInteropService.cs +++ b/DeepDrftPublic.Client/Services/AudioInteropService.cs @@ -16,6 +16,15 @@ public class AudioInteropService : IAsyncDisposable { try { + if (!await WaitForModuleReadyAsync()) + { + return new AudioOperationResult + { + Success = false, + Error = "Audio engine failed to load (timed out waiting for DeepDrftAudio module)." + }; + } + var result = await _jsRuntime.InvokeAsync("DeepDrftAudio.createPlayer", playerId); return result; } @@ -25,19 +34,34 @@ public class AudioInteropService : IAsyncDisposable } } - public async Task InitializeBufferedPlayerAsync(string playerId) + // The audio engine is loaded as an ES module via a deferred `import(...)` in + // App.razor, so on a slow WASM boot or cache miss it may not have executed by + // the time the first track is selected. Poll its readiness probe (tolerating + // the window before `window.DeepDrftAudio` even exists, when the call throws) + // up to a short timeout before the first interop call. + private async Task WaitForModuleReadyAsync() { - return await InvokeJsAsync("DeepDrftAudio.initializeBufferedPlayer", playerId); - } + const int timeoutMs = 5000; + const int pollIntervalMs = 50; + var elapsed = 0; - public async Task AppendAudioBlockAsync(string playerId, byte[] audioBlock) - { - return await InvokeJsAsync("DeepDrftAudio.appendAudioBlock", playerId, audioBlock); - } + while (elapsed < timeoutMs) + { + try + { + if (await _jsRuntime.InvokeAsync("DeepDrftAudio.isReady")) + return true; + } + catch + { + // window.DeepDrftAudio not attached yet — keep polling until timeout. + } - public async Task FinalizeAudioBufferAsync(string playerId) - { - return await InvokeJsAsync("DeepDrftAudio.finalizeAudioBuffer", playerId); + await Task.Delay(pollIntervalMs); + elapsed += pollIntervalMs; + } + + return false; } // Streaming methods @@ -238,8 +262,6 @@ public class AudioInteropService : IAsyncDisposable { if (typeof(T) == typeof(AudioOperationResult)) return (T)(object)new AudioOperationResult { Success = false, Error = ex.Message }; - if (typeof(T) == typeof(AudioLoadResult)) - return (T)(object)new AudioLoadResult { Success = false, Error = ex.Message }; if (typeof(T) == typeof(StreamingResult)) return (T)(object)new StreamingResult { Success = false, Error = ex.Message }; if (typeof(T) == typeof(SeekResult)) @@ -331,14 +353,6 @@ public class SeekResult : AudioOperationResult public long ByteOffset { get; set; } } -public class AudioLoadResult : AudioOperationResult -{ - public double Duration { get; set; } - public int SampleRate { get; set; } - public int NumberOfChannels { get; set; } - public double LoadProgress { get; set; } -} - public class StreamingResult : AudioOperationResult { public bool CanStartStreaming { get; set; } diff --git a/DeepDrftPublic.Client/Services/AudioPlayerService.cs b/DeepDrftPublic.Client/Services/AudioPlayerService.cs index 4fadea6..ca91536 100644 --- a/DeepDrftPublic.Client/Services/AudioPlayerService.cs +++ b/DeepDrftPublic.Client/Services/AudioPlayerService.cs @@ -2,7 +2,6 @@ using DeepDrftModels.DTOs; using DeepDrftPublic.Client.Clients; using Microsoft.AspNetCore.Components; using NetBlocks.Models; -using System.Buffers; namespace DeepDrftPublic.Client.Services; @@ -38,6 +37,9 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable public EventCallback? OnStateChanged { get; set; } public EventCallback? OnTrackSelected { get; set; } + /// + public event Action? StateChanged; + protected AudioPlayerService(AudioInteropService audioInterop, TrackMediaClient trackMediaClient) { _audioInterop = audioInterop; @@ -74,134 +76,16 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable } } - public virtual async Task SelectTrack(TrackDto track) - { - await EnsureInitializedAsync(); - - await NotifyStateChanged(); - - if (OnTrackSelected.HasValue) - await OnTrackSelected.Value.InvokeAsync(); - - await LoadTrack(track); - await NotifyStateChanged(); - } - - private async Task LoadTrack(TrackDto track) - { - try - { - if (IsLoading) return; - - if (IsPlaying || IsPaused) - { - await Unload(); - } - - // Reset state to indicate loading has started - ErrorMessage = null; - LoadProgress = 0; - IsLoaded = false; - IsLoading = true; - Duration = null; - CurrentTime = 0; - await NotifyStateChanged(); - - var loadResult = await _audioInterop.InitializeBufferedPlayerAsync(PlayerId); - if (loadResult?.Success != true) - { - ErrorMessage = $"Failed to initialize audio buffer: {loadResult?.Error ?? "Unknown error"}"; - return; - } - - var mediaResult = await _trackMediaClient.GetTrackMedia(track.EntryKey); - if (!mediaResult.Success) - { - ErrorMessage = mediaResult.GetMessage(); - return; - } - - if (mediaResult.Value == null) - { - ErrorMessage = "No audio returned from server"; - return; - } - - TrackMediaResponse audio = mediaResult.Value; - await StreamAudio(audio); - } - catch (Exception ex) - { - ErrorMessage = $"Error loading audio: {ex.Message}"; - LoadProgress = 0; - IsLoaded = false; - } - finally - { - IsLoading = false; - await NotifyStateChanged(); - } - } - - private async Task StreamAudio(TrackMediaResponse audio) - { - const int bufferSize = 32 * 1024; - var rentedBuffer = ArrayPool.Shared.Rent(bufferSize); - try - { - long totalBytesRead = 0; - int currentBytes; - - do - { - currentBytes = await audio.Stream.ReadAsync(rentedBuffer, 0, bufferSize); - - if (currentBytes > 0) - { - totalBytesRead += currentBytes; - - // Slice to actual bytes read before sending to interop - var chunk = rentedBuffer[..currentBytes]; - - var appendResult = await _audioInterop.AppendAudioBlockAsync(PlayerId, chunk); - if (!appendResult.Success) - { - throw new Exception($"Failed to append audio block: {appendResult.Error}"); - } - - if (audio.ContentLength > 0) - { - LoadProgress = Math.Min(1.0, (double)totalBytesRead / audio.ContentLength); - await NotifyStateChanged(); - } - } - } while (currentBytes > 0); - - var finalizeResult = await _audioInterop.FinalizeAudioBufferAsync(PlayerId); - if (!finalizeResult.Success) - { - throw new Exception($"Failed to finalize audio buffer: {finalizeResult.Error}"); - } - - Duration = finalizeResult.Duration; - LoadProgress = 1.0; - IsLoaded = true; - ErrorMessage = null; - await NotifyStateChanged(); - } - catch (Exception ex) - { - ErrorMessage = $"Error streaming audio: {ex.Message}"; - LoadProgress = 0; - IsLoaded = false; - await NotifyStateChanged(); - throw; - } - finally - { - ArrayPool.Shared.Return(rentedBuffer); - } - } + /// + /// Selecting a track is only supported through the streaming path. The former + /// base buffered implementation drove JS no-ops (initializeBufferedPlayer / + /// appendAudioBlock / finalizeAudioBuffer) and silently played + /// silence. Subclasses must override with a real load path (see + /// ). + /// + public virtual Task SelectTrack(TrackDto track) => + throw new NotSupportedException( + "The base buffered player path is not implemented. Use a streaming player (SelectTrackStreaming)."); public async Task TogglePlayPause() { @@ -377,7 +261,9 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable { IsPlaying = false; IsPaused = false; + IsLoaded = false; CurrentTime = 0; + Duration = null; await NotifyStateChanged(); } @@ -394,6 +280,7 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable { if (OnStateChanged.HasValue) await OnStateChanged.Value.InvokeAsync(); + StateChanged?.Invoke(); } protected async Task NotifyTrackSelected() diff --git a/DeepDrftPublic.Client/Services/IPlayerService.cs b/DeepDrftPublic.Client/Services/IPlayerService.cs index 6d71bcb..2234d16 100644 --- a/DeepDrftPublic.Client/Services/IPlayerService.cs +++ b/DeepDrftPublic.Client/Services/IPlayerService.cs @@ -22,6 +22,16 @@ public interface IPlayerService // Events for UI updates EventCallback? OnStateChanged { get; set; } EventCallback? OnTrackSelected { get; set; } + + /// + /// Multicast side-channel for state changes. The provider owns the single + /// EventCallback (it drives the provider re-render); + /// cascade consumers that read state directly off this service — and so are not + /// re-rendered by the provider's render when the cascade is IsFixed — + /// subscribe here to re-render themselves. Fires on the same cadence as + /// (throttled to ~10/s during streaming). + /// + event Action? StateChanged; // Control methods Task InitializeAsync(); diff --git a/DeepDrftPublic/Interop/audio/index.ts b/DeepDrftPublic/Interop/audio/index.ts index a037326..f399dfa 100644 --- a/DeepDrftPublic/Interop/audio/index.ts +++ b/DeepDrftPublic/Interop/audio/index.ts @@ -7,6 +7,10 @@ import { AudioPlayer, AudioResult, StreamingResult, AudioState } from './AudioPl // Player instances by ID const audioPlayers = new Map(); +// Readiness state, flipped true at the end of module execution once the API is +// attached to window. Read via DeepDrftAudio.isReady(). +let ready = false; + // .NET interop type interface DotNetObjectReference { invokeMethodAsync(methodName: string, ...args: unknown[]): Promise; @@ -204,18 +208,12 @@ const DeepDrftAudio = { return { success: false, error: 'Player not found' }; }, - // Legacy compatibility - these may not be needed but kept for safety - initializeBufferedPlayer: (_playerId: string): AudioResult => { - return { success: true }; // No-op for streaming mode - }, - - appendAudioBlock: (_playerId: string, _audioBlock: Uint8Array): AudioResult => { - return { success: true }; // No-op - use processStreamingChunk instead - }, - - finalizeAudioBuffer: async (_playerId: string): Promise => { - return { success: true }; // No-op for streaming mode - } + // Readiness probe — true once this module has finished executing and the API + // is attached to window. Blazor polls this before the first interop call so a + // slow WASM boot / cache miss does not surface as a generic init failure. + // Exposed as a method because Blazor JS interop invokes functions, not bare + // properties. + isReady: (): boolean => ready }; // Expose to window @@ -226,5 +224,8 @@ declare global { } window.DeepDrftAudio = DeepDrftAudio; +// Flip ready last so a poller that sees isReady() === true is guaranteed the +// whole surface is attached and callable. +ready = true; export { DeepDrftAudio }; diff --git a/DeepDrftPublic/Interop/audiobuffermanager.ts b/DeepDrftPublic/Interop/audiobuffermanager.ts deleted file mode 100644 index 4bbb4d0..0000000 --- a/DeepDrftPublic/Interop/audiobuffermanager.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * AudioBufferManager - Encapsulates all audio buffer storage and scheduling logic. - * - * Responsibilities: - * - Store decoded AudioBuffers (retained for pause/resume/seek) - * - Track playback position - * - Schedule buffers for playback from any position - * - Handle pause/resume without losing audio data - */ - -export interface ScheduledBuffer { - source: AudioBufferSourceNode; - startTime: number; // AudioContext time when this buffer starts - duration: number; // Duration of this buffer - bufferIndex: number; // Index in decodedBuffers array -} - -export class AudioBufferManager { - private decodedBuffers: AudioBuffer[] = []; - private scheduledSources: ScheduledBuffer[] = []; - private audioContext: AudioContext; - private gainNode: GainNode; - - // Playback state - private playbackStartTime: number = 0; // AudioContext.currentTime when playback started - private playbackStartPosition: number = 0; // Position in audio (seconds) where playback started - private nextScheduleIndex: number = 0; // Next buffer index to schedule during streaming - private nextScheduleTime: number = 0; // AudioContext time for next buffer - - // Callbacks - public onBufferEnded: (() => void) | null = null; - public onAllBuffersPlayed: (() => void) | null = null; - - constructor(audioContext: AudioContext, gainNode: GainNode) { - this.audioContext = audioContext; - this.gainNode = gainNode; - } - - /** - * Add a newly decoded buffer to storage - */ - addBuffer(buffer: AudioBuffer): void { - this.decodedBuffers.push(buffer); - console.log(`📦 Buffer added: index=${this.decodedBuffers.length - 1}, duration=${buffer.duration.toFixed(3)}s, total=${this.getTotalDuration().toFixed(3)}s`); - } - - /** - * Get total duration of all stored buffers - */ - getTotalDuration(): number { - return this.decodedBuffers.reduce((sum, b) => sum + b.duration, 0); - } - - /** - * Get number of stored buffers - */ - getBufferCount(): number { - return this.decodedBuffers.length; - } - - /** - * Get current playback position in seconds - */ - getCurrentPosition(): number { - if (this.playbackStartTime === 0) { - return this.playbackStartPosition; - } - const elapsed = this.audioContext.currentTime - this.playbackStartTime; - return this.playbackStartPosition + elapsed; - } - - /** - * Schedule playback from a specific position (used for play, resume, seek) - */ - scheduleFromPosition(position: number): void { - // Stop any currently scheduled sources - this.stopAllScheduled(); - - // Find which buffer contains this position - let accumulatedTime = 0; - let startBufferIndex = 0; - let offsetInBuffer = 0; - - for (let i = 0; i < this.decodedBuffers.length; i++) { - const bufferDuration = this.decodedBuffers[i].duration; - if (accumulatedTime + bufferDuration > position) { - startBufferIndex = i; - offsetInBuffer = position - accumulatedTime; - break; - } - accumulatedTime += bufferDuration; - startBufferIndex = i + 1; - } - - console.log(`🎯 Scheduling from position ${position.toFixed(3)}s: buffer[${startBufferIndex}] offset=${offsetInBuffer.toFixed(3)}s`); - - // Record playback start reference - this.playbackStartPosition = position; - this.playbackStartTime = this.audioContext.currentTime; - this.nextScheduleTime = this.audioContext.currentTime + 0.01; // Small lookahead - - // Schedule buffers starting from the found position - this.scheduleBuffersFrom(startBufferIndex, offsetInBuffer); - } - - /** - * Schedule pending buffers during live streaming (called when new buffers arrive) - */ - schedulePendingBuffers(): void { - if (this.nextScheduleIndex >= this.decodedBuffers.length) { - return; // No new buffers to schedule - } - - // If this is the first scheduling, initialize timing - if (this.nextScheduleTime === 0) { - this.nextScheduleTime = this.audioContext.currentTime + 0.01; - } - - this.scheduleBuffersFrom(this.nextScheduleIndex, 0); - } - - /** - * Internal: Schedule buffers starting from a specific index - */ - private scheduleBuffersFrom(startIndex: number, offsetInFirstBuffer: number): void { - const lookaheadTarget = 0.5; // Schedule up to 500ms ahead - - for (let i = startIndex; i < this.decodedBuffers.length; i++) { - const buffer = this.decodedBuffers[i]; - const isFirstBuffer = (i === startIndex && offsetInFirstBuffer > 0); - const offset = isFirstBuffer ? offsetInFirstBuffer : 0; - const duration = buffer.duration - offset; - - // Create and configure source - const source = this.audioContext.createBufferSource(); - source.buffer = buffer; - source.connect(this.gainNode); - - // Set up ended callback - const bufferIndex = i; - source.onended = () => this.handleBufferEnded(bufferIndex); - - // Schedule the source - source.start(this.nextScheduleTime, offset); - - // Track the scheduled source - this.scheduledSources.push({ - source, - startTime: this.nextScheduleTime, - duration, - bufferIndex: i - }); - - console.log(`🎵 Scheduled buffer[${i}]: start=${this.nextScheduleTime.toFixed(3)}s, offset=${offset.toFixed(3)}s, duration=${duration.toFixed(3)}s`); - - // Update timing for next buffer - this.nextScheduleTime += duration; - this.nextScheduleIndex = i + 1; - - // Check if we have enough lookahead - const lookahead = this.nextScheduleTime - this.audioContext.currentTime; - if (lookahead > lookaheadTarget) { - console.log(`📋 Sufficient lookahead: ${(lookahead * 1000).toFixed(0)}ms`); - break; - } - } - } - - /** - * Handle a buffer finishing playback - */ - private handleBufferEnded(bufferIndex: number): void { - // Remove from scheduled list - this.scheduledSources = this.scheduledSources.filter(s => s.bufferIndex !== bufferIndex); - - this.onBufferEnded?.(); - - // Check if all buffers have finished - if (this.scheduledSources.length === 0 && this.nextScheduleIndex >= this.decodedBuffers.length) { - console.log(`✓ All buffers played`); - this.onAllBuffersPlayed?.(); - } - } - - /** - * Stop all scheduled sources (for pause/stop) - */ - stopAllScheduled(): void { - for (const scheduled of this.scheduledSources) { - try { - scheduled.source.stop(); - } catch (e) { - // Source may already be stopped - } - } - this.scheduledSources = []; - console.log(`⏹️ Stopped all scheduled sources`); - } - - /** - * Pause playback - saves position and stops sources - */ - pause(): number { - const position = this.getCurrentPosition(); - this.stopAllScheduled(); - this.playbackStartPosition = position; - this.playbackStartTime = 0; - console.log(`⏸️ Paused at ${position.toFixed(3)}s`); - return position; - } - - /** - * Reset to beginning (for stop) - */ - resetToStart(): void { - this.stopAllScheduled(); - this.playbackStartPosition = 0; - this.playbackStartTime = 0; - this.nextScheduleIndex = 0; - this.nextScheduleTime = 0; - console.log(`⏮️ Reset to start`); - } - - /** - * Full reset - clears all buffers (for unload/new track) - */ - clear(): void { - this.stopAllScheduled(); - this.decodedBuffers = []; - this.playbackStartPosition = 0; - this.playbackStartTime = 0; - this.nextScheduleIndex = 0; - this.nextScheduleTime = 0; - console.log(`🗑️ Buffer manager cleared`); - } - - /** - * Check if we have any buffers - */ - hasBuffers(): boolean { - return this.decodedBuffers.length > 0; - } - - /** - * Check if we have enough buffers to start playback - */ - hasMinimumBuffers(minCount: number): boolean { - return this.decodedBuffers.length >= minCount; - } -} diff --git a/DeepDrftPublic/Interop/webaudio.ts b/DeepDrftPublic/Interop/webaudio.ts deleted file mode 100644 index 71fed02..0000000 --- a/DeepDrftPublic/Interop/webaudio.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * webaudio.ts - Legacy entry point for Blazor Audio Interop - * - * This file now delegates to the SOLID audio architecture in ./audio/ - * All functionality is provided by the new modular classes: - * - AudioContextManager: Web Audio API context and routing - * - StreamDecoder: WAV parsing and decoding - * - PlaybackScheduler: Buffer storage and playback scheduling - * - AudioPlayer: Main orchestrator - */ - -// Re-export from the new SOLID architecture -export { DeepDrftAudio } from './audio/index.js'; -export { AudioPlayer, AudioResult, StreamingResult, AudioState } from './audio/AudioPlayer.js';