interface AudioResult { success: boolean; error?: string; } interface LoadAudioResult extends AudioResult { duration?: number; sampleRate?: number; numberOfChannels?: number; loadProgress?: number; } import { WavHeader, WavUtils } from './wavutils.js'; interface StreamingResult extends AudioResult { canStartStreaming?: boolean; headerParsed?: boolean; bufferCount?: number; } interface AudioState { isPlaying: boolean; isPaused: boolean; currentTime: number; duration: number; volume: number; loadProgress: number; } type ProgressCallback = (currentTime: number) => void; type EndCallback = () => void; type DecodeSuccessCallback = (audioBuffer: AudioBuffer) => void; type DecodeErrorCallback = (error: DOMException) => void; declare global { interface Window { webkitAudioContext?: new() => AudioContext; DeepDrftAudio: typeof DeepDrftAudio; } interface AudioContext { decodeAudioData(audioData: ArrayBuffer | SharedArrayBuffer): Promise; decodeAudioData(audioData: ArrayBuffer | SharedArrayBuffer, successCallback?: DecodeSuccessCallback, errorCallback?: DecodeErrorCallback): Promise; } } 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 progressInterval: number | null = null; private bufferChunks: Uint8Array[] = []; private currentSize: number = 0; private processedBytes: number = 0; // Track how many bytes we've already processed // Streaming properties private isStreamingMode: boolean = false; private wavHeader: WavHeader | null = null; private bufferQueue: AudioBuffer[] = []; private currentStreamSource: AudioBufferSourceNode | null = null; private nextStartTime: number = 0; private streamingStarted: boolean = false; private streamingCompleted: boolean = false; // Track if streaming is finished private totalStreamLength: number = 0; // Total bytes expected in stream private minBuffersForStreaming: number = 6; // Increased for better buffering async initialize(): Promise { try { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (!AudioContextClass) { throw new Error('Web Audio API not supported'); } // Initialize with 44.1kHz for music (most common rate) to avoid recreation this.audioContext = new AudioContextClass({ sampleRate: 44100 }); this.gainNode = this.audioContext.createGain(); this.gainNode.connect(this.audioContext.destination); console.log(`AudioContext initialized: sampleRate=${this.audioContext.sampleRate}Hz, state=${this.audioContext.state}`); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } async ensureAudioContextReady(): Promise { try { if (this.audioContext!.state === 'suspended') { console.log('πŸ”Š Resuming AudioContext on track selection (user interaction)'); await this.audioContext!.resume(); console.log(`βœ… AudioContext resumed: state=${this.audioContext!.state}`); } return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } initializeBuffered(): AudioResult { try { this.bufferChunks = []; this.currentSize = 0; return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } appendAudioBlock(audioBlock: Uint8Array): AudioResult { try { this.bufferChunks.push(audioBlock); this.currentSize += audioBlock.length; return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } async finalizeAudioBuffer(): Promise { try { const arrayBuffer = new ArrayBuffer(this.currentSize); const view = new Uint8Array(arrayBuffer); let offset = 0; for (const chunk of this.bufferChunks) { view.set(chunk, offset); offset += chunk.length; } this.audioBuffer = await this.audioContext!.decodeAudioData(arrayBuffer); this.duration = this.audioBuffer.duration; this.bufferChunks = []; this.currentSize = 0; 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; } initializeStreaming(totalStreamLength: number): AudioResult { try { this.isStreamingMode = true; this.bufferChunks = []; this.bufferQueue = []; this.currentSize = 0; this.processedBytes = 0; // Reset stream position this.totalStreamLength = totalStreamLength; // Set total expected stream length this.wavHeader = null; this.streamingStarted = false; this.streamingCompleted = false; // Reset completion flag this.nextStartTime = 0; console.log(`Streaming initialized: expecting ${this.totalStreamLength} total bytes`); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } private chunkCounter = 0; async processStreamingChunk(audioChunk: Uint8Array): Promise { try { this.chunkCounter++; console.log(`\n=== CHUNK ${this.chunkCounter} ===`); console.log(`Incoming chunk size: ${audioChunk.length}`); console.log(`Chunk preview:`, Array.from(audioChunk.slice(0, 32)).map(b => b.toString(16).padStart(2, '0')).join(' ')); console.log(`Buffer queue length before processing: ${this.bufferQueue.length}`); await this.processChunk(audioChunk); // Check if we've received all expected data console.log(`Stream check: ${this.currentSize}/${this.totalStreamLength} bytes, completed=${this.streamingCompleted}`); if (this.totalStreamLength > 0 && this.currentSize >= this.totalStreamLength) { console.log(`Stream complete: received ${this.currentSize}/${this.totalStreamLength} bytes`); this.streamingCompleted = true; } const canStart = this.wavHeader !== null && this.bufferQueue.length >= this.minBuffersForStreaming; return { success: true, canStartStreaming: canStart, headerParsed: this.wavHeader !== null, bufferCount: this.bufferQueue.length }; } catch (error) { return { success: false, error: (error as Error).message }; } } private isFirstChunk = true; private async processChunk(audioChunk: Uint8Array): Promise { if (this.isFirstChunk) { const audioData = await this.extractAudioFromFirstChunk(audioChunk); this.addToAudioStream(audioData); this.isFirstChunk = false; } else { // Continuation chunks are pure audio data this.addToAudioStream(audioChunk); } await this.processAudioStream(); } private async extractAudioFromFirstChunk(chunkData: Uint8Array): Promise { console.log('\n--- EXTRACTING AUDIO FROM FIRST CHUNK ---'); // Parse header and setup AudioContext const header = WavUtils.parseHeader([chunkData], chunkData.length); if (!header) { throw new Error('Invalid WAV header in first chunk'); } this.wavHeader = header; console.log(`WAV format: ${header.bitsPerSample}-bit, ${header.channels}ch, ${header.sampleRate}Hz`); console.log(`Header details: blockAlign=${header.blockAlign}, byteRate=${header.byteRate}, headerSize=${header.headerSize}`); // Recreate AudioContext with correct sample rate if needed (only during initial setup) if (this.audioContext!.sampleRate !== header.sampleRate) { console.log(`πŸ”„ AudioContext sample rate mismatch: ${this.audioContext!.sampleRate}Hz -> ${header.sampleRate}Hz`); // Only recreate if we haven't started playing yet AND AudioContext is already running if (!this.streamingStarted && !this.isPlaying && this.audioContext!.state === 'running') { console.log(`⚠️ Recreating AudioContext for proper sample rate matching`); await this.audioContext!.close(); const AudioContextClass = window.AudioContext || window.webkitAudioContext; this.audioContext = new AudioContextClass({ sampleRate: header.sampleRate }); this.gainNode = this.audioContext.createGain(); this.gainNode.connect(this.audioContext.destination); console.log(`βœ… AudioContext recreated: ${this.audioContext.sampleRate}Hz (should eliminate resampling artifacts)`); } else { console.log(`ℹ️ Keeping existing AudioContext - using Web Audio API sample rate conversion`); } } // Extract pure audio data (skip WAV header) const audioData = chunkData.subarray(header.headerSize); console.log(`Extracted ${audioData.length} bytes of audio data (skipped ${header.headerSize} byte header)`); return audioData; } private async ensureCorrectSampleRate(sampleRate: number): Promise { if (this.audioContext!.sampleRate !== sampleRate) { console.log(`πŸ”Š AUDIO CONTEXT CHANGE START: ${this.audioContext!.sampleRate}Hz -> ${sampleRate}Hz`); console.log(`⚠️ This may cause an audible pop/click!`); await this.audioContext!.close(); console.log(`βœ… Old AudioContext closed`); const AudioContextClass = window.AudioContext || window.webkitAudioContext; this.audioContext = new AudioContextClass({ sampleRate }); console.log(`βœ… New AudioContext created: actual=${this.audioContext.sampleRate}Hz (requested=${sampleRate}Hz)`); this.gainNode = this.audioContext.createGain(); this.gainNode.connect(this.audioContext.destination); console.log(`πŸ”Š AUDIO CONTEXT CHANGE COMPLETE`); } } private addToAudioStream(audioData: Uint8Array): void { this.bufferChunks.push(audioData); this.currentSize += audioData.length; console.log(`Added ${audioData.length} bytes to audio stream (total: ${this.currentSize} bytes)`); } private async processAudioStream(): Promise { if (!this.wavHeader) return; // Process available data (but don't over-process during active playback) if (this.streamingStarted && this.bufferQueue.length >= 2) { console.log(`Buffer queue has cushion (${this.bufferQueue.length}), minimal processing`); // Still process but be less aggressive } // Create sample-aligned segments from continuous audio stream const maxSegmentSize = 64 * 1024; // 64KB segments to match C# chunks better const availableBytes = this.currentSize - this.processedBytes; // Only count unprocessed bytes const alignedSize = WavUtils.getSampleAlignedChunkSize(this.wavHeader, maxSegmentSize, availableBytes); if (alignedSize > 0) { console.log(`\n--- CREATING ALIGNED AUDIO SEGMENT ---`); console.log(`Available: ${availableBytes} bytes, requesting: ${alignedSize} bytes (frame-aligned, frame size: ${this.wavHeader.blockAlign})`); console.log(`Buffer queue: ${this.bufferQueue.length}, processing chunk`); // Extract sample-aligned segment from continuous stream const alignedSegment = this.extractAlignedData(alignedSize); const wavFile = this.createWavFromRawData(alignedSegment); await this.createAudioBufferFromChunk(wavFile); // Note: No longer removing processed data - we track position instead } } private extractAlignedData(alignedSize: number): Uint8Array { const extracted = new Uint8Array(alignedSize); let extractedOffset = 0; let remaining = alignedSize; let streamPosition = this.processedBytes; // Start from where we left off let currentPos = 0; for (const chunk of this.bufferChunks) { if (remaining <= 0) break; // Skip chunks that are entirely before our current stream position if (currentPos + chunk.length <= streamPosition) { currentPos += chunk.length; continue; } // Calculate the offset within this chunk to start extracting const chunkStartOffset = Math.max(0, streamPosition - currentPos); const availableInChunk = chunk.length - chunkStartOffset; const toCopy = Math.min(availableInChunk, remaining); if (toCopy > 0) { extracted.set(chunk.subarray(chunkStartOffset, chunkStartOffset + toCopy), extractedOffset); extractedOffset += toCopy; remaining -= toCopy; } currentPos += chunk.length; } // Update processed bytes position this.processedBytes += alignedSize; console.log(`Extracted ${alignedSize} bytes from stream position ${streamPosition} -> ${this.processedBytes}`); return extracted; } private removeProcessedData(processedSize: number): void { let remaining = processedSize; while (remaining > 0 && this.bufferChunks.length > 0) { const firstChunk = this.bufferChunks[0]; if (firstChunk.length <= remaining) { // Remove entire chunk remaining -= firstChunk.length; this.currentSize -= firstChunk.length; this.bufferChunks.shift(); } else { // Partially remove chunk const newChunk = firstChunk.subarray(remaining); this.bufferChunks[0] = newChunk; this.currentSize -= remaining; remaining = 0; } } } private concatenateChunks(): Uint8Array { const totalSize = this.currentSize; const concatenated = new Uint8Array(totalSize); let offset = 0; for (const chunk of this.bufferChunks) { concatenated.set(chunk, offset); offset += chunk.length; } return concatenated; } private createWavFromRawData(rawData: Uint8Array): Uint8Array { const header = WavUtils.createHeader(this.wavHeader!, rawData.length); const wavFile = new Uint8Array(header.length + rawData.length); wavFile.set(header, 0); wavFile.set(rawData, header.length); console.log(`Created WAV: header=${header.length} bytes, data=${rawData.length} bytes, total=${wavFile.length} bytes`); console.log(`Expected duration: ${rawData.length / this.wavHeader!.byteRate} seconds`); return wavFile; } startStreamingPlayback(): AudioResult { if (!this.wavHeader || this.bufferQueue.length === 0) { return { success: false, error: "Not ready for streaming playback" }; } try { console.log(`\n=== STARTING STREAMING PLAYBACK ===`); console.log(`AudioContext state: ${this.audioContext!.state}`); console.log(`AudioContext sample rate: ${this.audioContext!.sampleRate}Hz`); console.log(`Current time precision: ${this.audioContext!.currentTime.toFixed(6)}s`); console.log(`Queue ready: ${this.bufferQueue.length} buffers, ${this.bufferQueue.reduce((sum, b) => sum + b.duration, 0).toFixed(3)}s total`); // AudioContext should already be resumed during track selection const startTimestamp = performance.now(); const audioContextTime = this.audioContext!.currentTime; this.streamingStarted = true; this.isPlaying = true; this.isPaused = false; this.nextStartTime = audioContextTime; this.startTime = this.nextStartTime; console.log(`▢️ Playback timing: audioContext=${audioContextTime.toFixed(6)}s, performance=${startTimestamp.toFixed(3)}ms`); console.log(`🎡 Initial nextStartTime set to: ${this.nextStartTime.toFixed(6)}s`); this.scheduleNextBuffer(); this.startProgressTracking(); console.log(`βœ… Streaming playback started successfully`); console.log(`=====================================\n`); return { success: true }; } catch (error) { console.error(`❌ Failed to start streaming playback:`, error); return { success: false, error: (error as Error).message }; } } private async createAudioBufferFromChunk(chunkData: Uint8Array): Promise { try { console.log(`createAudioBufferFromChunk: chunkData.length=${chunkData.length}`); // Create a clean ArrayBuffer with exact size (avoid reusable buffer issues) const cleanBuffer = new ArrayBuffer(chunkData.length); new Uint8Array(cleanBuffer).set(chunkData); console.log(`Decoding ${cleanBuffer.byteLength} bytes with Web Audio API`); console.log('Starting decode...'); // Try with timeout to catch hanging decodes const decodePromise = this.audioContext!.decodeAudioData(cleanBuffer); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Decode timeout after 5 seconds')), 5000); }); const audioBuffer = await Promise.race([decodePromise, timeoutPromise]); console.log("AFTER Promise.race - this should always appear after 5 seconds max"); console.log(`\n--- DECODE SUCCESS ---`); console.log(`Buffer duration: ${audioBuffer.duration}s`); console.log(`Buffer channels: ${audioBuffer.numberOfChannels}`); console.log(`Buffer sample rate: ${audioBuffer.sampleRate}`); console.log(`Buffer length: ${audioBuffer.length} samples`); // Check if buffer contains actual audio data or silence/noise const channel0 = audioBuffer.getChannelData(0); const firstSamples = Array.from(channel0.slice(0, 10)).map(v => v.toFixed(4)); const maxValue = Math.max(...Array.from(channel0).map(Math.abs)); const avgValue = Array.from(channel0).reduce((sum, val) => sum + Math.abs(val), 0) / channel0.length; console.log(`First 10 samples:`, firstSamples); console.log(`Max amplitude: ${maxValue.toFixed(4)}`); console.log(`Average amplitude: ${avgValue.toFixed(4)}`); this.bufferQueue.push(audioBuffer); console.log(`\n=== BUFFER QUEUE UPDATE ===`); console.log(`βœ“ Added buffer: duration=${audioBuffer.duration.toFixed(6)}s, samples=${audioBuffer.length}`); console.log(`Queue state: ${this.bufferQueue.length} buffers (${this.bufferQueue.map(b => b.duration.toFixed(3)).join('s, ')}s)`); console.log(`Total queued audio: ${this.bufferQueue.reduce((sum, b) => sum + b.duration, 0).toFixed(3)}s`); console.log(`Streaming: started=${this.streamingStarted}, completed=${this.streamingCompleted}`); console.log(`Current playback time: ${this.audioContext!.currentTime.toFixed(6)}s`); // Schedule immediately when streaming has started (for gapless playback) if (this.streamingStarted) { console.log(`⏩ Triggering proactive schedule (streaming active)`); this.scheduleNextBuffer(); } else { console.log(`⏸️ Not scheduling yet (streaming not started)`); } console.log(`===========================\n`); } catch (error) { console.error('Error creating audio buffer from chunk:', error); console.error('Failed chunk size:', chunkData.length); // Log first few bytes of the chunk for debugging const preview = Array.from(chunkData.slice(0, 16)).map(b => b.toString(16).padStart(2, '0')).join(' '); console.error('Chunk preview (first 16 bytes):', preview); } } private scheduleNextBuffer(): void { // Schedule all available buffers proactively instead of waiting for onended while (this.bufferQueue.length > 0 && this.streamingStarted) { const scheduleStartTime = performance.now(); const buffer = this.bufferQueue.shift()!; const source = this.audioContext!.createBufferSource(); source.buffer = buffer; source.connect(this.gainNode!); // Critical: Use precise timing for gapless playback const currentTime = this.audioContext!.currentTime; // For the very first buffer, add small lookahead to avoid startup glitches const startTime = this.nextStartTime > 0 ? this.nextStartTime : currentTime + 0.01; const schedulingDelay = currentTime - startTime; console.log(`🎡 Scheduling buffer: start=${startTime.toFixed(3)}s, duration=${buffer.duration.toFixed(3)}s, delay=${(schedulingDelay * 1000).toFixed(1)}ms ${schedulingDelay > 0.005 ? '⚠️' : 'βœ“'}, queue=${this.bufferQueue.length}`); // Only log timing issues for debugging const gap = Math.abs(startTime - this.nextStartTime); if (gap > 0.001) { console.warn(`⚠️ TIMING GAP: ${(gap * 1000).toFixed(3)}ms between expected and actual start time`); } source.onended = () => { const endTime = this.audioContext!.currentTime; const expectedEndTime = startTime + buffer.duration; const timingError = Math.abs(endTime - expectedEndTime); console.log(`🏁 Buffer ended: timing error=${(timingError * 1000).toFixed(1)}ms`); this.currentStreamSource = null; // Check for end-of-stream if (this.bufferQueue.length === 0) { if (this.streamingCompleted) { console.log(`βœ“ End-of-stream: All buffers played at ${endTime.toFixed(3)}s (expected)`); } else { console.warn(`❌ Buffer underrun! Queue empty at ${endTime.toFixed(3)}s (unexpected during streaming)`); } if (!this.isPlaying) { this.onEndCallback?.(); } } }; source.start(startTime); // Calculate next start time with sample-perfect precision this.nextStartTime = startTime + buffer.duration; this.currentStreamSource = source; const scheduleEndTime = performance.now(); const scheduleProcessingTime = scheduleEndTime - scheduleStartTime; // Stop scheduling when we have enough buffered ahead const lookaheadTime = this.nextStartTime - currentTime; if (lookaheadTime > 0.5) { // Stop when we have 500ms of audio scheduled ahead console.log(`πŸ“‹ Sufficient lookahead: ${(lookaheadTime * 1000).toFixed(0)}ms scheduled ahead`); break; } } } unload(): AudioResult { try { this.stop(); this.audioBuffer = null; this.duration = 0; this.bufferChunks = []; this.currentSize = 0; this.processedBytes = 0; // Reset stream position // Clean up streaming state this.isStreamingMode = false; this.wavHeader = null; this.bufferQueue = []; this.streamingStarted = false; this.streamingCompleted = false; this.totalStreamLength = 0; this.nextStartTime = 0; if (this.currentStreamSource) { this.currentStreamSource.stop(); this.currentStreamSource = null; } return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } } 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; this.bufferChunks = []; this.currentSize = 0; // Clean up streaming state this.bufferQueue = []; this.wavHeader = null; this.currentStreamSource = 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 }; } }, initializeBufferedPlayer: (playerId: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.initializeBuffered(); }, appendAudioBlock: (playerId: string, audioBlock: Uint8Array): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.appendAudioBlock(audioBlock); }, finalizeAudioBuffer: async (playerId: string): Promise => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return await player.finalizeAudioBuffer(); }, // Streaming methods initializeStreaming: (playerId: string, totalStreamLength: number): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.initializeStreaming(totalStreamLength); }, processStreamingChunk: async (playerId: string, audioChunk: Uint8Array): Promise => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return await player.processStreamingChunk(audioChunk); }, startStreamingPlayback: (playerId: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.startStreamingPlayback(); }, ensureAudioContextReady: async (playerId: string): Promise => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return await player.ensureAudioContextReady(); }, 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(); }, unload: (playerId: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) { return { success: false, error: "Player not found" }; } return player.unload(); }, 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 }; }, 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;