import { RealtimeProvider } from '../providers/RealtimeProvider'; import { EnhancedVADManager } from './EnhancedVADManager'; /** * Audio Manager configuration */ export interface AudioManagerConfig { /** * Called when AI speaking state changes */ onAISpeakingChange?: (isSpeaking: boolean) => void; } /** * Audio Manager class * Handles microphone setup, audio streaming, and playback */ export declare class AudioManager { private refs; private nextStartTime; private audioSources; private typingSoundSources; private providerRef; private config; private isAISpeaking; private playbackGeneration; private provisionalInterruptPaused; private provisionalInterruptAudioQueue; private provisionalInterruptResumeTimer; private isMuted; private selectedDeviceId; private currentDevice; private enhancedVADManager; private frameTimestamp; private clientVADAudioBuffer; private rollingAudioBuffer; private rollingAudioBufferMaxSize; private isCurrentlySpeaking; constructor(config?: AudioManagerConfig); private createOutputAudioContext; private ensureOutputAudioContext; /** * Initialize audio contexts for input and output * Also loads the AudioWorklet processor module * * CRITICAL for iOS: This must be called synchronously within a user gesture handler. * iOS Safari requires AudioContext creation and resume() to happen within the same * user gesture event handler. Any async operations between gesture and resume() * will cause the AudioContext to remain suspended. */ initAudio(): Promise; /** * Set up RTC loopback connection for echo cancellation * * CRITICAL: This routes BOTH microphone input AND TTS output through the SAME * RTCPeerConnection loopback, simulating a real WebRTC peer-to-peer call. * This is essential for Chrome's Acoustic Echo Cancellation (AEC) to work. * * Architecture: * - Mic → RTC Connection (sender) → RTC Loopback Connection (receiver) → MediaStream → Web Audio API → Server * - TTS → Web Audio API → MediaStreamDestination → RTC Connection (sender) → RTC Loopback Connection (receiver) → Audio Element → Speakers * * Both streams go through the SAME RTCPeerConnection, allowing Chrome's AEC to * correlate input/output and cancel echo. * * @param micStream - The microphone MediaStream to add to the loopback * * Reference: https://gist.github.com/alexciarlillo/4b9f75516f93c10d7b39282d10cd17bc */ private setupRTCLoopback; /** * Wait for ICE connection to be established * Returns true if connection succeeds, false if it fails or times out */ private waitForICEConnection; /** * Clean up RTC loopback resources */ private cleanupRTCLoopback; /** * Setup microphone and start streaming to session * @param provider - The realtime provider to stream audio to * @param onStatusUpdate - Callback for status updates * @param deviceId - Optional device ID to use (if not provided, uses default or previously selected device) * * CRITICAL for iOS: This must be called within a user gesture handler chain. * getUserMedia() requires user gesture on iOS Safari. */ setupMicrophone(provider: RealtimeProvider, onStatusUpdate?: (status: string) => void, deviceId?: string): Promise; /** * Decode base64 audio data to Uint8Array */ private decode; /** * Decode audio data for playback */ private decodeAudioData; /** * Play audio data (from AI response) * @param audioData - Either base64 string or ArrayBuffer */ playAudio(audioData: string | ArrayBuffer): Promise; beginProvisionalInterrupt(): void; endProvisionalInterrupt(discardQueuedAudio?: boolean): Promise; /** * Play typing sound segment * Uses separate audio source queue to allow immediate stopping * @param audioData - PCM16 audio data (Uint8Array) * @param volume - Volume multiplier (0.0 to 1.0) * @returns AudioBufferSourceNode instance */ playTypingSound(audioData: Uint8Array, volume?: number): AudioBufferSourceNode | null; /** * Stop all typing sounds immediately */ stopTypingSounds(): void; /** * Stop all audio playback (e.g., on interruption) */ stopAllAudio(): void; /** * Get available audio input devices * * Note: On iOS Safari, getUserMedia must be called from a user gesture context * to get device labels. If permission hasn't been granted, this will return * devices with empty labels. * * @param requirePermission - If true, request getUserMedia first (requires user gesture on iOS) * @returns Promise resolving to array of MediaDeviceInfo for audio inputs */ getAvailableDevices(requirePermission?: boolean): Promise; /** * Check if microphone permission has been granted * @returns Promise resolving to true if permission granted, false otherwise */ hasMicrophonePermission(): Promise; /** * Request microphone permission * MUST be called from a user gesture handler on iOS Safari * @returns Promise resolving to true if permission granted, false otherwise */ requestMicrophonePermission(): Promise; /** * Get the currently active microphone device * @returns MediaDeviceInfo for current device, or null if not available */ getCurrentDevice(): MediaDeviceInfo | null; /** * Switch to a different microphone device * This will reinitialize the microphone stream with the new device * @param deviceId - The device ID to switch to * @param provider - The realtime provider (required if microphone is already set up) * @param onStatusUpdate - Optional status update callback */ switchDevice(deviceId: string, provider?: RealtimeProvider, onStatusUpdate?: (status: string) => void): Promise; /** * Cleanup all audio resources */ cleanup(): void; /** * Get the media stream for VAD integration * * CRITICAL: Returns the SERVER receiver side stream (micLoopbackStream) if available, * which is what the SERVER receives from the CLIENT. This ensures VAD processes * audio that has gone through the RTC loopback and AEC. * * Falls back to original mediaStream if RTC loopback is not set up. */ getMediaStream(): MediaStream | null; /** * Check if audio is initialized */ isInitialized(): boolean; /** * Check if microphone is streaming */ isStreaming(): boolean; /** * Mute audio input (stop sending audio to server) * WebSocket stays connected, but no audio data is transmitted */ setMuted(muted: boolean): void; /** * Check if audio input is muted */ isMutedState(): boolean; /** * Set EnhancedVADManager reference for client-side VAD frame processing * Called by SessionManager when EnhancedVADManager is initialized */ setEnhancedVADManager(vadManager: EnhancedVADManager | null): void; } //# sourceMappingURL=AudioManager.d.ts.map