/** * Audio Interop - Exposes AudioPlayer to Blazor via window.DeepDrftAudio */ import { AudioPlayer, AudioResult, StreamingResult, AudioState } from './AudioPlayer.js'; import { canDecodeOggOpus } from './OpusCapability.js'; // 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; } // Global API exposed to 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 }; } }, initializeStreaming: async (playerId: string, totalStreamLength: number, contentType: string): Promise => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.initializeStreaming(totalStreamLength, contentType); }, // Opus injection seam (wave 18.4). Wave 18.5 fetches the per-track sidecar (setup header + // seek index) over HTTP and hands the raw bytes here BEFORE initializeStreaming on an Opus // stream. This module never fetches the sidecar — it only parses + stores it on the player. setOpusSidecar: (playerId: string, sidecarBytes: Uint8Array): AudioResult => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.setOpusSidecar(sidecarBytes); }, // Capability seam. Resolves whether this browser can stream-decode Ogg Opus via WebCodecs // (AudioDecoder + codec:'opus'; Safari < 16.4 / older Firefox cannot). The player consumes this // to choose lossless when unsupported; this module only reports the capability. canDecodeOggOpus: (): Promise => canDecodeOggOpus(), processStreamingChunk: async (playerId: string, chunk: Uint8Array): Promise => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.processStreamingChunk(chunk); }, startStreamingPlayback: async (playerId: string): Promise => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.startStreamingPlayback(); }, markStreamComplete: async (playerId: string): Promise => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.markStreamComplete(); }, ensureAudioContextReady: async (playerId: string): Promise => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.ensureAudioContextReady(); }, play: async (playerId: string): Promise => { 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); }, // New methods for seek-beyond-buffer support getBufferedDuration: (playerId: string): number => { const player = audioPlayers.get(playerId); return player?.getBufferedDuration() ?? 0; }, calculateByteOffset: (playerId: string, positionSeconds: number): number => { const player = audioPlayers.get(playerId); return player?.calculateByteOffset(positionSeconds) ?? 0; }, // "Load at timestamp" seam (Phase 18 wave 18.6 format switch). Resolve the file-absolute byte offset // to begin a stream at `position` with no playback/buffer state — the C# load-from-position path calls // this after initializeStreaming (Opus: sidecar resolves immediately; WAV: after a header probe) and // then streams from the returned offset via the seek/refill loop. seekBeyondBuffer:true + byteOffset. resolveStreamOffset: (playerId: string, position: number): AudioResult => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.resolveStreamOffset(position); }, // Phase 21.2a back-pressure poll: the C# read loop calls this WHILE throttled to learn when // the scheduler has drained below low-water and reading may resume. A missing player reads as // "not paused" so a torn-down player never wedges a loop that is already exiting. isProductionPaused: (playerId: string): boolean => { const player = audioPlayers.get(playerId); return player?.isProductionPaused() ?? false; }, reinitializeFromOffset: (playerId: string, totalStreamLength: number, seekPosition: number): AudioResult => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.reinitializeFromOffset(totalStreamLength, seekPosition); }, // Phase 21.3 / AC6: recover into a clean paused-but-loaded state after a window-miss refill // (seek-back past the retained tail) failed its Range fetch or reinit. Prevents the starved // scheduler from firing a silent false end; leaves the track loaded so a retry is possible. recoverFromFailedRefill: (playerId: string, seekPosition: number): AudioResult => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; return player.recoverFromFailedRefill(seekPosition); }, 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); return player?.getCurrentTime() ?? 0; }, getState: (playerId: string): AudioState | null => { const player = audioPlayers.get(playerId); return player?.getState() ?? null; }, setOnProgressCallback: ( playerId: string, dotNetRef: DotNetObjectReference, methodName: string ): AudioResult => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; player.setOnProgressCallback((currentTime: number) => { dotNetRef.invokeMethodAsync(methodName, currentTime); }); return { success: true }; }, setOnEndCallback: ( playerId: string, dotNetRef: DotNetObjectReference, methodName: string ): AudioResult => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; player.setOnEndCallback(() => { dotNetRef.invokeMethodAsync(methodName); }); 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 }; }, getLevelDb: (playerId: string): number => { const player = audioPlayers.get(playerId); return player?.getLevelDb() ?? -Infinity; }, startLevelAnimation: ( playerId: string, callbackId: string, dotNetRef: DotNetObjectReference, methodName: string ): AudioResult => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; player.startLevelAnimation(callbackId, (db: number) => { dotNetRef.invokeMethodAsync(methodName, db); }); return { success: true }; }, stopLevelAnimation: (playerId: string, callbackId: string): AudioResult => { const player = audioPlayers.get(playerId); if (!player) return { success: false, error: 'Player not found' }; player.stopLevelAnimation(callbackId); 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' }; }, // 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 declare global { interface Window { DeepDrftAudio: typeof DeepDrftAudio; } } 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 };