
    Sjr                       d Z ddlmZ ddlZddlZddlZddlZddlmZ ddl	Z
ddlZ	 ddlmZ dZ ej$                         Z ej$                         Zda ej,                         Zej0                  j3                  dd	      Zej0                  j3                  d
d      Zd&dZd'dZd(dZd(dZd&dZ d&dZ!d'dZ"d)d*dZ#dZ$dZ% ee&      jO                         jP                  jP                  dz  dz  Z) ee&      jO                         jP                  jP                  dz  dz  Z* G d d      Z+d+dZ,d,dZ-d-d.dZ. G d de+      Z/ G d  d!e+      Z0 G d" d#e+      Z1 G d$ d%      Z2y# e$ r dZY ?w xY w)/u;  Full-duplex audio: continuous 16 kHz mic capture + interruptible playback.

Mic runs in a callback thread and feeds an asyncio queue of 32 ms frames
(512 samples @ 16 kHz — the frame size Silero VAD expects).

Playback keeps its own OutputStream; stop() cuts audio within one block
(~10 ms) for instant barge-in.
    )annotationsN)Path)keyboardTFMUTE_KEYz<ctrl>+<alt>+m	BREAK_KEYz<ctrl>+<alt>+.c                 R    t         j                          t        dt         d       y)z7Request an immediate abort of the current turn/control.u&   
  BREAK — aborting current action ()N)_breaksetprintBREAK_HOTKEY     </Users/ahmed/devFolder/Ultron/claude-voice/voice/audio_io.pysignal_breakr   ,   s    
JJL	3L>
CDr   c                 V    t         j                         rt         j                          yy)z@True (and clears) if a break was requested since the last check.TF)r
   is_setclearr   r   r   break_requestedr   2   s    }}r   c                   t         5  | rt        j                          nt        j                          t        }ddd       	 |j                  |        yy# 1 sw Y   xY w# t        $ r}t        d|        Y d}~yd}~ww xY w)zCSet the mute flag AND release / re-acquire the physical mic device.Nz#  [mute] mic device toggle failed: )
_mute_lock
_mic_mutedr   r   _active_mic_on_mute_change	Exceptionr   )mutedmes      r   _apply_muter   :   ss    	NN 
 	}	=e$  
  	=7s;<<	=s#   2AA# A #	B,A??Bc                    t         j                         t        |       k(  ryt        |        t	        | rd       ydt
         d       y)uH   Public entry — hotkey, HUD mic button, or control file all route here.Nu?   
  MIC MUTED — device released (earbuds back to hi-fi output)u'   
  MIC UNMUTED — device re-acquired (r	   )r   r   boolr   r   MUTE_HOTKEY)r   s    r   	set_mutedr#   I   sF    d5k)	 M E4[M
CEr   c                 @    t        t        j                                 y N)r#   r   r   r   r   r   _toggle_muter&   S   s    *##%%&r   c                 ~   t         r\	 t        j                  t        t        t
        t        i      } d| _        | j                          t        dt         dt
         d       nt        d       dd	}t        j                  |d
      }|j                          y# t        $ r}t        d| d       Y d}~Id}~ww xY w)a  Global hotkey (Ctrl+Alt+M) + terminal fallback ('m'+Enter).

    The global hotkey needs macOS Input Monitoring/Accessibility for the
    terminal app (System Settings -> Privacy & Security); if that's not
    granted it silently receives nothing, so the stdin fallback matters.
    Tz  mute hotkey: u    · break/abort: z; (global; needs Input Monitoring) or type m/stop+Enter herez  [mute] global hotkey failed (z); use m+EnterNz;  [mute] pynput missing; mute with m+Enter in this terminalc                     dd l } | j                  D ]>  }|j                         j                         }|dv rt	                0|dv s5t                @ y )Nr   )r   muteunmute)stopbreakabortx)sysstdinstriplowerr&   r   )r/   linews      r   
stdin_loopz&_start_mute_hotkey.<locals>.stdin_loopk   sC    IID

""$A++55 r   targetdaemonreturnNone)
_PYNPUT_OK_kbGlobalHotKeysr"   r&   r   r   r8   startr   r   	threadingThread)hkr   r5   ts       r   _start_mute_hotkeyrD   W   s     	G""K$0,$@ ABBIHHJOK=0A, PO O P
 	KL 	
48AGGI  	G3A3nEFF	Gs   AB 	B<#B77B<c                 *    t         j                         S r%   )r   r   r   r   r   is_mutedrF   x   s    r   c                2   ddl }|j                  dk7  ry	 t        j                  g dddd      }t	        |j
                  j                               }|| k  r0t        j                  dd	d
|  gdd       t        d| d|         yy# t        $ r Y yw xY w)u   Apple's VoiceProcessingIO permanently drags the system input gain
    down (documented behavior) — a day of sessions left it at 27/100 and
    the assistant went deaf. Self-heal at startup.r   Ndarwin)	osascript-ez%input volume of (get volume settings)T   capture_outputtexttimeoutrI   rJ   zset volume input volume rM   rO   z  input volume was u&   /100 (VPIO gain-drag) — restored to )	r/   platform
subprocessrunintstdoutr1   r   r   )floorr/   outcurrents       r   restore_input_volumerY   |   s     
||xnnHdA
 cjj&&()U?NNd&>ug$FG#Q 'y 1!!&) *   s   A2B
 
	BBi>  i   helperaecmicsysaudioc                  (    e Zd ZdZddZddZddZy)_FrameQueuez8Shared drop-oldest frame delivery into the asyncio loop.c                H    || _         t        j                  d      | _        y )N   )maxsize)_loopasyncioQueueframes)selfloops     r   __init__z_FrameQueue.__init__   s    
18s1Kr   c                   	 | j                   j                  |       y # t        j                  $ rS 	 | j                   j	                          | j                   j                  |       Y y # t        j
                  $ r Y Y y w xY ww xY wr%   )re   
put_nowaitrc   	QueueFull
get_nowait
QueueEmptyrf   frames     r   _put_nowaitz_FrameQueue._put_nowait   sm    	KK""5)   	&&(&&u-%% 	s&    B5A))B <B?B  Bc                p    	 | j                   j                  | j                  |       y # t        $ r Y y w xY wr%   )rb   call_soon_threadsaferp   RuntimeErrorrn   s     r   _deliverz_FrameQueue._deliver   s3    	JJ++D,<,<eD 		s   &) 	55Nrg   zasyncio.AbstractEventLoop)ro   
np.ndarrayr:   r;   )__name__
__module____qualname____doc__rh   rp   rt   r   r   r   r^   r^      s    BL	r   r^   c                 `  
 	 t        j                         } t        j                  j                  dd      j                         }|r	 t        |      S 	 t         j                  j                  d   }d}d\  }}t        |       D ]  \  }}|d   dk  r|d   j                         
d	}t        
fd
|D              r|dz  }d
v sd
v r|dz  }d
v r|dz  }	 t        j                  |d         d   j                         }	d|	v r|dz  }n	d|	v r|dz  }||k(  r|dz  }||kD  s||}} |S # t        $ r Y yw xY w# t        $ rI t        |       D ]7  \  }}|d   dkD  s|j                         |d   j                         v s3|c cY S  Y 8w xY w# t        $ r d}Y ,w xY w# t        $ r Y w xY w)u  Choose the microphone.

    MIC_DEVICE env wins (an index, or a name substring). Otherwise score all
    input devices: real microphones beat virtual ones (Steam Streaming,
    VB-Cable, Stereo Mix, Sound Mapper...), and WASAPI beats MME/DirectSound —
    Windows wireless-headset mics through MME often deliver audio too quiet
    for the VAD to ever trigger. Returns a device index, or None = default.
    N
MIC_DEVICE max_input_channelsr   name
steamvirtualcableloopbackvoicemeeterz
stereo mixzsound mapperzprimary soundwavezline in)Ng    e        c              3  &   K   | ]  }|v  
 y wr%   r   .0bns     r   	<genexpr>z%_pick_input_device.<locals>.<genexpr>        #s!qAvs   d   
microphonemic
   headsetrK   hostapiwasapi   wdm-ks   )sdquery_devicesr   osenvirongetr1   rT   
ValueError	enumerater2   defaultdeviceanyquery_hostapis)devswantid
default_inbadbest_ibest_ssapir   s             @r   _pick_input_devicer      s   ! ::>>,+113D	t9ZZ&&q)
MCNFF$1!"a'fIOO#s##HA1
GA>FA	##AiL1&9??AC3QSQ 
?FAv:FF-  . MS    	!$1*+a/ JJLAfIOO,==H (	  
,  		sY   D+ 
D: F <F!+	D76D7:"F#FFFFFF!	F-,F-c                   
 	 t        j                         } g }d}	 t         j                  j                  d   }t        |       D ]  \  }}|d   dk  r|d   j                         
d}t        
fd|D              r|dz  }d	
v sd

v r|dz  }d
v r|dz  }	 t        j                  |d         d   j                         }d|v r|dz  }n	d|v r|dz  }||k(  r|dz  }|dkD  s|j                  ||f        |j                  d       |D cg c]  \  }}|	 }	}}|	j                  d       |	S # t        $ r dgcY S w xY w# t        $ r d}Y w xY w# t        $ r Y w xY wc c}}w )a"  All plausible mics, best first, ending with the system default (None).

    A device can EXIST yet deliver pure zeros (a wireless dongle with the
    headset off, a dead WASAPI endpoint...), so callers should PROBE each
    candidate and take the first one that produces actual signal.
    Nr   r   r~   r   r   c              3  &   K   | ]  }|v  
 y wr%   r   r   s     r   r   z$_input_candidates.<locals>.<genexpr>  r   r   r   r   r   r   r   rK   r   r   r   r      r   iT)reverse)r   r   r   r   r   r   r2   r   r   appendsort)r   scoredr   r   r   r   r   r   _srW   r   s             @r   _input_candidatesr      s   ! ')FMCZZ&&q)
 $1!"a'fIOO#s##HA1
GA>FA	##AiL1&9??AC3QSR 
?FAs7MM1a&!-  . KKK,23F52qQFC3JJtJG  v  
&  		 4s:   D2 E <EE%2E EEE	E"!E"c                    	 t        j                  t        |t        z        t        dd|       }t        j                          t        t        j                  |      j                               S # t        $ r Y yw xY w)u   Record briefly from `dev`; return the peak. A LIVE mic always shows
    ambient noise > 0 — an exact 0.0 means the endpoint is dead/silent.   float32)
sampleratechannelsdtyper         )
r   recrT   SAMPLE_RATEwaitfloatnpabsmaxr   )devsecondsr   s      r   _probe_devicer     s_    ffS;./Ky>
	RVVC[__&'' s   A-A0 0	A<;A<c                  h     e Zd ZdZdZdZdZd fdZddZd Z	ddZ
dd	Zedd
       ZddZ xZS )	MicStreamai  Raw mic capture (sounddevice) with device auto-pick + auto-gain.

    Auto-gain: wireless/USB headset mics on Windows often capture speech at
    ~0.03 peak, far below what the VAD reads as voice. We track the rolling
    peak and scale frames toward a working level before the VAD sees them.
    MIC_GAIN env forces a fixed gain instead (e.g. MIC_GAIN=6).
    g333333?g       @g~jth?c                "   t         |   |       d | _        d | _        t        j
                  j                  dd      j                         j                         }|r|dk7  rt        |      nd | _
        | j                  xs d| _        d| _        y )NMIC_GAINr}   auto      ?r   )superrh   _stream_devicer   r   r   r1   r2   r   _fixed_gain_gain
_roll_peak)rf   rg   g	__class__s      r   rh   zMicStream.__init__2  ss    .2#'JJNN:r*00288:'(Q&[58d%%,
r   c                   | j                   $t        j                  || j                   z  dd      S t        t        j                  |      j                               }t        || j                  dz        | _        | j                  | j                  kD  rRt        | j                  | j                  | j                  z        }| xj                  d|| j                  z
  z  z  c_        || j                  z  }t        t        j                  |      j                               }|dkD  r'| xj                  d|z  z  c_        || j                  z  }t        j                  |dd      S )Nr   r   gX9v?g?g333333?)r   r   clipr   r   r   r   _FLOORmin	_MAX_GAIN_TARGETr   )rf   ro   peakr7   rW   outpeaks         r   _agczMicStream._agc;  s
   '7754#3#33T3??RVVE]&&()dDOOe$;<??T[[()GHFJJ#$**!455Jdjj  s)*T>JJ$.(J$**$CwwsD#&&r   c                    t         j                         ry | j                  | j                  |d d df         j	                  t
        j                               y )Nr   )r   r   rt   r   astyper   r   )rf   indata_frames_timestatuss        r   	_callbackzMicStream._callbackN  s=    diiq!t-44RZZ@Ar   c                   |r9| j                   d c}| _         |"	 |j                          |j                          y y | j                   X	 t	        j
                  | j                  t        ddt        | j                        | _         | j                   j                          y y # t        $ r Y y w xY w# t        $ r}t        d|        Y d }~y d }~ww xY w)Nr   r   r   r   r   r   	blocksizecallbackz$  [mic] reopen after unmute failed: )r   r+   closer   r   InputStreamr   r   FRAME_SAMPLESr   r?   r   )rf   r   r   r   s       r   r   zMicStream._on_mute_changeS  s     "llDOAt|}FFHaggi 
 \\!B!~~<<K!#}!^^ 
 ""$ " !   B<QC@AABs*    B! 	AB0 !	B-,B-0	C9CCc           	     <   t         j                  j                  dd      j                         }|r|j	                         s|j                         }	 t        t        j                               D cg c]$  \  }}|d   dkD  r||d   j                         v r|& }}}g }|D ]"  }t        |      dkD  s|j                  |       $ ||z  }|st               g}n|rt        |      g}ng }g }t               D ]x  }t        |      }	| j                  |      }
|	dkD  r&t!        d|
 d|	d	d
       |j                  |        n1t!        d|
 d|	dk(  rdnd        |	dk(  sh|j                  |       z |j#                  |       |sd g}d }|D ]  }	 t        j$                  |t&        ddt(        | j*                        | _        | j,                  j/                          || _        | a| j                  |      | _        | j6                  rd| j6                   nd}t!        d| j4                   d| d        y  t9        d| d
      c c}}w # t        $ r g }Y w xY w# t        $ r4}|}t!        d| j                  |       d|        d | _        Y d }~
d }~ww xY w)Nr|   r}   r~   r   r   r   z  mic probe: u    — live (peak z.4fr	   u    — silentunavailabler   r   r   zfixed xr   z  mic device: z (gain: z$; MIC_DEVICE / MIC_GAIN to override)z  mic open failed on z: zno usable microphone ()r   r   r   r1   isdigitr2   r   r   r   r   r   r   r   rT   r   	_dev_namer   extendr   r   r   r   r   r?   r   r   device_namer   rs   )rf   forcedr   r   r   matches
candidatesr   r   r   r   last_errgainr   s                 r   r?   zMicStream.starth  s   b1779&..* <<>D)223C3C3E)F :)FA 459#qy'88 )F :
 J %+%%c*  '!J023
,/K=J
 J')F(*$S)~~c*#:M$/?SzKL%%c*dV5%)S[mDF G3;MM#& + f%"V
%)C$!~~*#+!^^  ""$""#'>>##6 8<8H8H'$"2"2!34# t'7'7&8 G; < =' 0 3H:Q?@@y:  j  $-dnnS.A-B"QCHI#$s=    I 2)II B$II II	J')JJc                    	 | | nt         j                  j                  d   }t        j                  |      d   S # t        $ r Y yw xY w)Nr   r   r   )r   r   r   r   r   )r   idxs     r   r   zMicStream._dev_name  sI    	#bjj.?.?.BC##C(00 		s   8; 	AAc                    | j                   <| j                   j                          | j                   j                          d | _         y y r%   r   r+   r   rf   s    r   r+   zMicStream.stop  8    <<#LLLL DL $r   ru   )ro   rv   r:   rv   r   r!   r:   r;   r9   )r   
int | Noner:   str)rw   rx   ry   rz   r   r   r   rh   r   r   r   r?   staticmethodr   r+   __classcell__r   s   @r   r   r   %  sP     GIF'&B
B*DAL   r   r   c                  f     e Zd ZdZd
 fdZedd       ZddZddZddZ	ddZ
ddZdd	Z xZS )AECMicStreamug  Echo-cancelled mic via helper/aecmic (Apple VoiceProcessingIO).

    Audio played by this Mac — including our own TTS — is subtracted
    from the mic signal in hardware-adjacent DSP, so barge-in detection
    never hears the assistant's own voice. Silence between utterances is
    gated to literal zeros by Apple's noise suppression; that's normal.
    c                    t         |   |       d | _        d | _        t	        j
                         | _        t	        j
                         | _        t        t        j                  j                  dd            | _        y )NAEC_GAINz1.0)r   rh   _proc_threadr@   Event	_stopping_waker   r   r   r   r   rf   rg   r   s     r   rh   zAECMicStream.__init__  sW    .2
04"*__&
 2::>>*e<=
r   c                 *    t         j                         S r%   )
AEC_HELPERis_filer   r   r   	availablezAECMicStream.available  s    !!##r   c                    | a t        j                         s| j                          t	        j
                  | j                  d      | _        | j                  j                          y )NTr6   )	r   r   r   _spawnr@   rA   _readerr  r?   r   s    r   r?   zAECMicStream.start  sD      "KKM ''t||DIr   c                p   | j                   yt        t              g}t        j                  j                  d      dk(  r|j                  d       	 t        j                  |t        j                  t        j                  d      | _         y# t        $ r}t        d|        d| _         Y d}~yd}~ww xY w)z@Launch the aecmic helper (acquires the mic / VoiceProcessingIO).N
AEC_BYPASS1z--bypassr   rU   stderrbufsizez  [aecmic] spawn failed: )r  r  r  r   r   r   r   rR   PopenPIPEDEVNULLr   r   )rf   argsr   s      r   r  zAECMicStream._spawn  s    ::!J ::>>,'3.KK
#	#))Z__Z5G5GDJ  	-aS12DJJ	s   :B 	B5B00B5c                r    | j                   dc}| _         |	 |j                          yy# t        $ r Y yw xY w)u   Terminate the helper — RELEASES the mic device so Bluetooth
        earbuds drop the call profile and go back to hi-fi output.N)r  	terminater   rf   ps     r   _killzAECMicStream._kill  sA     

D4:=   s   * 	66c                \    |r| j                          | j                  j                          y r%   )r&  r  r   )rf   r   s     r   r   zAECMicStream._on_mute_change  s     JJL

r   c                v   t         dz  }t         t        z  }t        j                  t         t        j                        }d}| j
                  j                         st        j                         rY| j                          d}| j                  |       | j                  j                  |       | j                  j                          | j                  }|<| j                          d}| j                  }|| j                  j                  d       	 |j                  j!                  |      }|s| j                          ||z  }t%        |      |k\  rt        j&                  |d | t        j(                        }||d  }t        j                         rnd|j+                  t        j                        | j,                  dz  z  }| j                  t        j.                  |dd             t%        |      |k\  r| j
                  j                         sy y # t"        $ r | j                          Y w xY w)Nr   r   r   g?      @r   r   )r   r   r   zerosr   r  r   r   r&  rt   r  r   r   r  r  rU   readr   len
frombufferint16r   r   r   )	rf   frame_bytes	frame_dursilencebufprocdatapcm16ro   s	            r   r  zAECMicStream._reader  s   #a'!K/	((=

;..'')  " 

g&

	*

  "::D|zz<JJOOC({{''4 

4KCc(k)c,;&7rxxH+,'$$&RZZ0DJJ4HIbggeT378 c(k)7 ..''))(  

s   H H87H8c                    t         | u rd a | j                  j                          | j                  j                          | j	                          y r%   )r   r  r   r  r&  r   s    r   r+   zAECMicStream.stop*  s6    $K



r   ru   r:   r!   r9   r  )rw   rx   ry   rz   rh   r  r  r?   r  r&  r   r  r+   r  r  s   @r   r  r    s@    > $ $ &9Pr   r  c                  V     e Zd ZdZd fdZed	d       Zd
dZd
dZd
dZ	d
dZ
 xZS )SystemAudioStreamua  System-audio (loopback) capture via helper/sysaudio (ScreenCaptureKit).

    Taps what the Mac is PLAYING — the remote voices of an online meeting come
    out of the speakers — cleanly and at full quality. This is the "them" side
    of a meeting: the AEC mic deliberately erases speaker output, so the far end
    is only audible here. Emits the same 16 kHz int16 mono frames as AECMicStream.

    Deliberately simple: NOT part of the mute-hotkey logic (a meeting recording
    must not be muted by the mic hotkey) and no AGC (digital audio is already
    leveled) — just start/stop and clip to [-1,1]. On SCK failure (no
    Screen-Recording grant when spawned from a plain terminal) the helper exits
    with a reason on stderr and NO frames arrive; last_error surfaces it so the
    caller can tell Ahmed instead of silently recording his side only.
    c                    t         |   |       d | _        d | _        d | _        t        j                         | _        d| _        y )Nr}   )	r   rh   r  r  
_errthreadr@   r  r  
last_errorr  s     r   rh   zSystemAudioStream.__init__C  s:    .2
0437"*r   c                 *    t         j                         S r%   )SYSAUDIO_HELPERr  r   r   r   r  zSystemAudioStream.availableK  s    &&((r   c                   	 t        j                  t        t              gt         j                  t         j                  d      | _        t        j                  | j                  d      | _        | j                  j                          t        j                  | j                  d      | _        | j                  j                          y # t        $ r/}t        |      | _        t        d|        d | _        Y d }~y d }~ww xY w)Nr   r  z  [sysaudio] spawn failed: Tr6   )rR   r  r  r?  r  r  r   r=  r   r@   rA   r  r  r?   
_drain_errr<  )rf   r   s     r   r?   zSystemAudioStream.startO  s    		#))_%&z!DJ !''t||DI $**$//$O  	!!fDO/s34DJ		s   AC 	D%C>>Dc                   | j                   }||j                  y t        |j                  j                  d      D ]O  }|j	                  dd      j                         }|s&d|vs+|j                  dd      j                         | _        Q y )Nr   zutf-8replacerunningz	sysaudio:r}   )r  r  iterreadlinedecoder1   rC  r=  )rf   r4  rawr3   s       r   rA  zSystemAudioStream._drain_erra  sy    zz<4;;.,,c2C::gy1779D$"&,,{B"?"E"E"G 3r   c                F   t         dz  }| j                  }||j                  y d}| j                  j	                         s	 |j                  j                  |      }|sy ||z  }t        |      |k\  rt        j                  |d | t        j                        }||d  }|j                  t        j                        dz  }| j                  t        j                  |dd             t        |      |k\  r| j                  j	                         sy y # t        $ r Y y w xY w)Nr   r   r)  r*  r   r   )r   r  rU   r  r   r,  r   r-  r   r.  r/  r   r   rt   r   )rf   r0  r4  r3  r5  r6  ro   s          r   r  zSystemAudioStream._readerm  s    #a'zz<4;;...''){{''4 4KCc(k)c,;&7rxxH+,'RZZ07:bggeT378	 c(k) ..'')  s   D 	D D c                    | j                   j                          | j                  d c}| _        |	 |j                          y y # t        $ r Y y w xY wr%   )r  r   r  r#  r   r$  s     r   r+   zSystemAudioStream.stop  sO    

D4:=   s   A 	AAru   r8  r9   )rw   rx   ry   rz   rh   r  r  r?   rA  r  r+   r  r  s   @r   r:  r:  3  s6     ) ) $
H9(r   r:  c                  d    e Zd ZdZddZd ZdddZddZddZddZ	dd	Z
edd
       ZdddZy)Speakerz7Plays queued PCM chunks; stop() silences within ~10 ms.c                    || _         t        j                  dt        j                        | _        t        j                         | _        d | _        d| _	        y )Nr   r)  F)
r   r   r+  r   _bufferr@   Lock_lockr   _active)rf   r   s     r   rh   zSpeaker.__init__  s;    $xx4^^%
/3r   c                (   | j                   5  t        |t        | j                              }| j                  d | |d |df<   d||d df<   | j                  |d  | _        t        | j                        dk(  rd| _        d d d        y # 1 sw Y   y xY w)Nr   r   F)rP  r   r-  rN  rQ  )rf   outdatare   r   _statusr   s         r   r   zSpeaker._callback  s}    ZZFC-.A!\\"1-GBQBEN GABEN<<+DL4<< A%$ ZZs   A2BBNc           	         t        j                  | j                  dddd|| j                        | _        | j                  j                          y )Nr   r   r   low)r   r   r   r   latencyr   r   )r   OutputStreamr   r   r   r?   )rf   r   s     r   _openzSpeaker._open  sA    ^^
 	r   c                "   ddl }	 | j                          y# t        $ r%}t        dt	        |      dd  d       Y d}~nd}~ww xY w|j
                  dk(  r	 ddl}|j                  d      xs d}t        j                  j                  |      rvt        j                  |d	d
dgddd      j                  j                         }t        d |D        d      }|r,t        j                  |d
dd|gdd       t        d| d       n# t        $ r Y nw xY wt!        d      D ]l  }	 | j                           y# t        $ rL}|dk(  r(t        dt	        |      dd  d       d| _        Y d}~ yddl}|j'                  d       Y d}~fd}~ww xY w y)un  Open the output stream. The default output can be a flaky Bluetooth
        device (e.g. disconnected CMF Buds) that fails with a PortAudio error —
        that used to CRASH the whole engine at startup. So on failure we switch
        the system default output to the built-in speakers and retry, and if
        even that fails we degrade to mute rather than die.r   Nz"  speaker: default output failed (<   u"   ) — trying the built-in speakersrH   SwitchAudioSourcez#/opt/homebrew/bin/SwitchAudioSourcez-az-toutputTrK   rL   c              3     K   | ]8  }d |j                         v sd|j                         v r|j                          : yw)macbookzbuilt-inN)r2   r1   )r   os     r   r   z Speaker.start.<locals>.<genexpr>  s9      $@t!'0AGGI'='1QWWY'> %&GGIts   >A z-srP   z  speaker: switched output to ''   r   z&  speaker: could not open any output (u6   ) — Jarvis will be MUTE until an output device worksg      ?)r/   rY  r   r   r  rQ   shutilwhichr   pathexistsrR   rS   rU   
splitlinesnextranger   timesleep)	rf   r/   r   rc  sasallobuiltinattempt_ts	            r   r?   zSpeaker.start  s    		2JJL 	26s1vcr{m D1 1 2 2	2 <<8#||$78 @? 77>>#&%>>3dH*E9=D2355;VJJL  # $@t $@AEGG "T8T7'K6:AG ?yJK QxG


 a<B3q6#2;- PR R S#'DL!	  sE    	AA  AB.D 	DD%D77	F 'F-FFc                    | j                   <| j                   j                          | j                   j                          d | _         y y r%   r   r   s    r   stop_streamzSpeaker.stop_stream  r   r   c                    | j                   5  t        j                  | j                  |j	                  t        j
                        g      | _        d| _        ddd       y# 1 sw Y   yxY w)z.Queue audio (float32 mono at self.samplerate).TN)rP  r   concatenaterN  r   r   rQ  )rf   pcms     r   playzSpeaker.play  s@    ZZ>>4<<BJJ9O*PQDLDL ZZs   AA!!A*c                    | j                   5  t        j                  dt        j                        | _        d| _        ddd       y# 1 sw Y   yxY w)z6Barge-in: drop everything queued, silence immediately.r   r)  FN)rP  r   r+  r   rN  rQ  r   s    r   cutzSpeaker.cut  s0    ZZ88ARZZ8DL DL ZZs   2AAc                    | j                   S r%   )rQ  r   s    r   speakingzSpeaker.speaking  s    ||r   c                   K   d}| j                   rQt        j                  |       d{    ||z  }||k\  r| j                          t	        d       y| j                   rPyy7 9w)zWait for playback to drain. The timeout is a fail-safe: if the
        output device wedges (never drains), cut instead of hanging the
        whole conversation loop in 'speaking' forever.r   Nu7          [speaker: playback wedged — cut after timeout])rz  rc   rk  rx  r   )rf   pollrO   waiteds       r   	wait_donezSpeaker.wait_done  sX      mm--%%%dNF 
OP mm%s   'A%A#6A%!A%)r   rT   r%   r9   )ru  rv   r:   r;   r8  )g?g      ^@)r|  r   rO   r   r:   r;   )rw   rx   ry   rz   rh   r   rY  r?   rr  rv  rx  propertyrz  r~  r   r   r   rL  rL    sB    A%
*X  !  r   rL  r9   r8  r  )F   )rV   rT   r:   r;   )r:   r  )r:   zlist[int | None])g333333?)r   r  r   r   r:   r   )3rz   
__future__r   rc   r   rR   r@   pathlibr   numpyr   sounddevicer   pynputr   r=   r<   ImportErrorr  r   r
   r   rO  r   r   r   r"   r   r   r   r   r#   r&   rD   rF   rY   r   r   __file__resolveparentr  r?  r^   r   r   r   r   r  r:  rL  r   r   r   <module>r     s   #  	     &J Y__
		Y^^
jjnnZ)9: zz~~k+;<E=E'B. (^##%,,33h>I
x.((*11888CjP 24n,^	U  U ps; slU Upl lg  Js   E% %E0/E0