interface AudioResult { success: boolean; error?: string; } interface LoadAudioResult extends AudioResult { duration?: number; sampleRate?: number; numberOfChannels?: number; loadProgress?: number; } interface AudioState { isPlaying: boolean; isPaused: boolean; currentTime: number; duration: number; volume: number; loadProgress: number; } type ProgressCallback = (currentTime: number) => void; type EndCallback = () => void; type LoadProgressCallback = (progress: number) => void; interface Window { webkitAudioContext?: typeof AudioContext; DeepDrftAudio: typeof DeepDrftAudio; } class AudioPlayer { private audioContext: AudioContext | null = null; private audioBuffer: AudioBuffer | null = null; private source: AudioBufferSourceNode | null = null; private gainNode: GainNode | null = null; private isPlaying: boolean = false; private isPaused: boolean = false; private startTime: number = 0; private pauseOffset: number = 0; private duration: number = 0; private onProgressCallback: ProgressCallback | null = null; private onEndCallback: EndCallback | null = null; private onLoadProgressCallback: LoadProgressCallback | null = null; private progressInterval: number | null = null; async initialize(): Promise { try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); this.gainNode = this.audioContext.createGain(); this.gainNode.connect(this.audioContext.destination); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } async loadAudioFromUrl(url: string): Promise { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentLength = response.headers.get('Content-Length'); const reader = response.body?.getReader(); if (reader && contentLength) { // Stream with progress tracking const total = parseInt(contentLength, 10); let loaded = 0; const chunks: Uint8Array[] = []; // Initial progress if (this.onLoadProgressCallback) { this.onLoadProgressCallback(0); } let readAttempts = 0; const maxReadAttempts = 10000; // Prevent infinite loop while (readAttempts < maxReadAttempts) { try { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; const progress = (loaded / total) * 100; if (this.onLoadProgressCallback) { this.onLoadProgressCallback(progress); } readAttempts++; } catch (readerError) { break; } } // Combine chunks into single ArrayBuffer const arrayBuffer = new ArrayBuffer(loaded); const view = new Uint8Array(arrayBuffer); let offset = 0; for (const chunk of chunks) { view.set(chunk, offset); offset += chunk.length; } this.audioBuffer = await this.audioContext!.decodeAudioData(arrayBuffer); this.duration = this.audioBuffer.duration; // Final progress if (this.onLoadProgressCallback) { this.onLoadProgressCallback(100); } } else { // Fallback to original method if streaming not possible const arrayBuffer = await response.arrayBuffer(); this.audioBuffer = await this.audioContext!.decodeAudioData(arrayBuffer); this.duration = this.audioBuffer.duration; // Report 100% immediately for non-streaming responses if (this.onLoadProgressCallback) { this.onLoadProgressCallback(100); } } return { success: true, duration: this.duration, sampleRate: this.audioBuffer.sampleRate, numberOfChannels: this.audioBuffer.numberOfChannels, loadProgress: 100 }; } catch (error) { return { success: false, error: (error as Error).message }; } } play(): AudioResult { if (!this.audioBuffer) { return { success: false, error: "No audio loaded" }; } try { if (this.audioContext!.state === 'suspended') { this.audioContext!.resume(); } this.source = this.audioContext!.createBufferSource(); this.source.buffer = this.audioBuffer; this.source.connect(this.gainNode!); this.source.onended = () => { this.isPlaying = false; this.isPaused = false; this.startTime = 0; this.pauseOffset = 0; if (this.onEndCallback) { this.onEndCallback(); } }; if (this.isPaused) { this.source.start(0, this.pauseOffset); this.startTime = this.audioContext!.currentTime - this.pauseOffset; } else { this.source.start(0); this.startTime = this.audioContext!.currentTime; } this.isPlaying = true; this.isPaused = false; this.startProgressTracking(); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } pause(): AudioResult { if (!this.isPlaying) { return { success: false, error: "Audio is not playing" }; } try { this.source!.stop(); this.pauseOffset += this.audioContext!.currentTime - this.startTime; this.isPlaying = false; this.isPaused = true; this.stopProgressTracking(); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } stop(): AudioResult { try { if (this.source) { this.source.stop(); } this.isPlaying = false; this.isPaused = false; this.startTime = 0; this.pauseOffset = 0; this.stopProgressTracking(); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } seek(position: number): AudioResult { if (!this.audioBuffer || position < 0 || position > this.duration) { return { success: false, error: "Invalid seek position" }; } try { const wasPlaying = this.isPlaying; if (this.isPlaying) { this.source!.stop(); } this.pauseOffset = position; if (wasPlaying) { this.source = this.audioContext!.createBufferSource(); this.source.buffer = this.audioBuffer; this.source.connect(this.gainNode!); this.source.onended = () => { this.isPlaying = false; this.isPaused = false; this.startTime = 0; this.pauseOffset = 0; if (this.onEndCallback) { this.onEndCallback(); } }; this.source.start(0, position); this.startTime = this.audioContext!.currentTime - position; } else { this.isPaused = true; } return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } setVolume(volume: number): AudioResult { if (!this.gainNode) { return { success: false, error: "Audio not initialized" }; } try { const clampedVolume = Math.max(0, Math.min(1, volume)); this.gainNode.gain.setValueAtTime(clampedVolume, this.audioContext!.currentTime); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } getCurrentTime(): number { if (!this.isPlaying && !this.isPaused) { return 0; } if (this.isPlaying) { return Math.min(this.pauseOffset + (this.audioContext!.currentTime - this.startTime), this.duration); } else { return this.pauseOffset; } } getState(): AudioState { return { isPlaying: this.isPlaying, isPaused: this.isPaused, currentTime: this.getCurrentTime(), duration: this.duration, volume: this.gainNode ? this.gainNode.gain.value : 0, loadProgress: 100 }; } private startProgressTracking(): void { this.stopProgressTracking(); this.progressInterval = setInterval(() => { if (this.onProgressCallback) { this.onProgressCallback(this.getCurrentTime()); } }, 100); } private stopProgressTracking(): void { if (this.progressInterval) { clearInterval(this.progressInterval); this.progressInterval = null; } } setOnProgressCallback(callback: ProgressCallback): void { this.onProgressCallback = callback; } setOnEndCallback(callback: EndCallback): void { this.onEndCallback = callback; } setOnLoadProgressCallback(callback: LoadProgressCallback): void { this.onLoadProgressCallback = callback; } dispose(): void { this.stop(); this.stopProgressTracking(); if (this.audioContext && this.audioContext.state !== 'closed') { this.audioContext.close(); } this.audioContext = null; this.audioBuffer = null; this.gainNode = null; } } // Global player instances const audioPlayers = new Map(); // Define .NET interop types interface DotNetObjectReference { invokeMethodAsync(methodName: string, ...args: any[]): Promise; } // JavaScript interop functions for Blazor const DeepDrftAudio = { createPlayer: async (playerId: string): Promise => { try { const player = new AudioPlayer(); const result = await player.initialize(); if (result.success) { audioPlayers.set(playerId, player); } return result; } catch (error) { return { success: false, error: (error as Error).message }; } }, loadAudioFromUrl: async (playerId: string, url: string): Promise => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return await player.loadAudioFromUrl(url); }, play: (playerId: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.play(); }, pause: (playerId: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.pause(); }, stop: (playerId: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.stop(); }, seek: (playerId: string, position: number): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.seek(position); }, setVolume: (playerId: string, volume: number): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.setVolume(volume); }, getCurrentTime: (playerId: string): number => { const player = audioPlayers.get(playerId); if (!player) { return 0; } return player.getCurrentTime(); }, getState: (playerId: string): AudioState | null => { const player = audioPlayers.get(playerId); if (!player) { return null; } return player.getState(); }, setOnProgressCallback: (playerId: string, dotNetObjectReference: DotNetObjectReference, methodName: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } player.setOnProgressCallback((currentTime: number) => { dotNetObjectReference.invokeMethodAsync(methodName, currentTime); }); return { success: true }; }, setOnEndCallback: (playerId: string, dotNetObjectReference: DotNetObjectReference, methodName: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } player.setOnEndCallback(() => { dotNetObjectReference.invokeMethodAsync(methodName); }); return { success: true }; }, setOnLoadProgressCallback: (playerId: string, dotNetObjectReference: DotNetObjectReference, methodName: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } player.setOnLoadProgressCallback((progress: number) => { dotNetObjectReference.invokeMethodAsync(methodName, progress); }); return { success: true }; }, disposePlayer: (playerId: string): AudioResult => { const player = audioPlayers.get(playerId); if (player) { player.dispose(); audioPlayers.delete(playerId); return { success: true }; } return { success: false, error: "Player not found" }; } }; // Assign to window for global access window.DeepDrftAudio = DeepDrftAudio;