@@ -102,15 +102,17 @@ export class AudioMediaManager {
102102 }
103103 }
104104
105- const track = this . localStream . getAudioTracks ( ) [ 0 ] ;
106- if ( track ) {
107- track . enabled = true ;
108-
109- // Store the actual device being used when none was explicitly selected
110- const settings = track . getSettings ( ) ;
111- if ( settings . deviceId && ! this . agentConfig . inputOptions . device ) {
112- this . agentConfig . inputOptions . device = settings . deviceId ;
113- }
105+ const localAudioTrack = this . localStream . getAudioTracks ( ) [ 0 ] ;
106+ if ( ! localAudioTrack ) {
107+ throw new Error ( "[AudioMediaManager] Microphone capture returned no audio track" ) ;
108+ }
109+
110+ localAudioTrack . enabled = true ;
111+
112+ // Store the actual device being used when none was explicitly selected.
113+ const localAudioTrackSettings = localAudioTrack . getSettings ( ) ;
114+ if ( localAudioTrackSettings . deviceId && ! this . agentConfig . inputOptions . device ) {
115+ this . agentConfig . inputOptions . device = localAudioTrackSettings . deviceId ;
114116 }
115117
116118 // Audio context for visualization - use vendor-prefixed version if needed
@@ -165,7 +167,7 @@ export class AudioMediaManager {
165167 const base : MediaTrackConstraints = {
166168 echoCancellation : true ,
167169 noiseSuppression : true ,
168- autoGainControl : false ,
170+ autoGainControl : true ,
169171 } ;
170172
171173 if ( this . agentConfig . inputOptions . device ) {
@@ -183,11 +185,9 @@ export class AudioMediaManager {
183185 channelCount : { ideal : 1 } ,
184186 echoCancellation : true ,
185187 noiseSuppression : true ,
186- // Disable AGC for voice AI: when the assistant finishes speaking and the
187- // user starts talking, AGC would abruptly boost the mic gain (it was
188- // suppressed while output was playing), clipping the first syllable and
189- // causing a jarring volume jump. The STT model handles normalization.
190- autoGainControl : false ,
188+ // Keep AGC enabled to prevent low microphone capture levels on quieter
189+ // mics and mobile hardware.
190+ autoGainControl : true ,
191191 } ;
192192
193193 if ( this . agentConfig . inputOptions . device ) {
@@ -213,8 +213,8 @@ export class AudioMediaManager {
213213 ...base ,
214214 // @ts -ignore Chrome/Edge-specific
215215 googEchoCancellation : true ,
216- // @ts -ignore AGC disabled — matches autoGainControl: false above
217- googAutoGainControl : false ,
216+ // @ts -ignore Chrome/Edge-specific AGC.
217+ googAutoGainControl : true ,
218218 // @ts -ignore
219219 googNoiseSuppression : true ,
220220 // @ts -ignore removes low-frequency rumble (fan/AC noise)
@@ -362,9 +362,19 @@ export class AudioMediaManager {
362362 // the load algorithm (which internally pauses), so play() must wait until
363363 // the element is ready or AbortError follows.
364364 if ( el . readyState < 3 /* HAVE_FUTURE_DATA */ ) {
365- await new Promise < void > ( ( resolve ) => {
366- el . addEventListener ( 'canplay' , resolve as EventListener , { once : true } ) ;
367- setTimeout ( resolve , 200 ) ;
365+ await new Promise < void > ( ( resolvePromise ) => {
366+ let isPromiseResolved = false ;
367+ // Resolve from whichever arrives first: `canplay` or fallback timeout.
368+ const resolveOnce = ( ) => {
369+ if ( isPromiseResolved ) return ;
370+ isPromiseResolved = true ;
371+ clearTimeout ( fallbackTimeoutId ) ;
372+ el . removeEventListener ( "canplay" , handleCanPlay ) ;
373+ resolvePromise ( ) ;
374+ } ;
375+ const handleCanPlay : EventListener = ( ) => resolveOnce ( ) ;
376+ const fallbackTimeoutId = setTimeout ( resolveOnce , 200 ) ;
377+ el . addEventListener ( "canplay" , handleCanPlay , { once : true } ) ;
368378 } ) ;
369379 }
370380
@@ -417,9 +427,19 @@ export class AudioMediaManager {
417427 // complete before resuming. 200 ms timeout guards against streams
418428 // with no active audio tracks where canplay may never fire.
419429 if ( el . readyState < 3 /* HAVE_FUTURE_DATA */ ) {
420- await new Promise < void > ( ( resolve ) => {
421- el . addEventListener ( 'canplay' , resolve as EventListener , { once : true } ) ;
422- setTimeout ( resolve , 200 ) ;
430+ await new Promise < void > ( ( resolvePromise ) => {
431+ let isPromiseResolved = false ;
432+ // Resolve from whichever arrives first: `canplay` or fallback timeout.
433+ const resolveOnce = ( ) => {
434+ if ( isPromiseResolved ) return ;
435+ isPromiseResolved = true ;
436+ clearTimeout ( fallbackTimeoutId ) ;
437+ el . removeEventListener ( "canplay" , handleCanPlay ) ;
438+ resolvePromise ( ) ;
439+ } ;
440+ const handleCanPlay : EventListener = ( ) => resolveOnce ( ) ;
441+ const fallbackTimeoutId = setTimeout ( resolveOnce , 200 ) ;
442+ el . addEventListener ( "canplay" , handleCanPlay , { once : true } ) ;
423443 } ) ;
424444 }
425445
@@ -504,6 +524,12 @@ export class AudioMediaManager {
504524 }
505525 }
506526
527+ const localAudioTrack = this . localStream . getAudioTracks ( ) [ 0 ] ;
528+ if ( ! localAudioTrack ) {
529+ throw new Error ( "[AudioMediaManager] Microphone device switch returned no audio track" ) ;
530+ }
531+ localAudioTrack . enabled = ! this . isMuted ;
532+
507533 // Reconnect analyser (disconnect old source first to avoid leaking audio nodes)
508534 if ( this . audioContext && this . _inputAnalyser && this . localStream ) {
509535 this . _inputAnalyser . disconnect ( ) ;
@@ -554,7 +580,7 @@ export class AudioMediaManager {
554580
555581 /** Get the new audio track after setInputDevice (caller wires it to peer) */
556582 getLocalAudioTrack ( ) : MediaStreamTrack | undefined {
557- return this . localStream ?. getAudioTracks ( ) [ 0 ] ;
583+ return this . localStream ?. getAudioTracks ( ) . find ( track => track . readyState === "live" ) ;
558584 }
559585
560586 // ---------------------------------------------------------------------------
0 commit comments