2222CHUNK_BYTES = CHUNK_SAMPLES * 2
2323
2424
25+ MAX_CAPTURE_RETRIES = 8
26+ RETRY_BACKOFF_BASE = 0.5 # seconds; doubles each attempt up to a cap
27+ RETRY_BACKOFF_MAX = 5.0
28+
29+
2530def _capture_alsa (
2631 device : str ,
2732 out_queue : "queue.Queue[Optional[bytes]]" ,
@@ -31,63 +36,104 @@ def _capture_alsa(
3136 """Capture from ALSA device via arecord; put PCM chunks in out_queue. Runs in thread.
3237 Uses plughw when device is hw:X,Y so ALSA can do sample-rate conversion (many USB mics only support 48kHz).
3338 If proc_holder is a list, the subprocess is stored as proc_holder[0] so the caller can terminate it to release the device quickly.
39+
40+ Auto-restarts arecord up to MAX_CAPTURE_RETRIES times when the device
41+ disappears transiently (e.g. USB bus contention with a camera).
3442 """
3543 dev = (device or "default" ).strip ()
3644 if dev .startswith ("hw:" ) and not dev .startswith ("plughw:" ):
3745 dev = "plug" + dev
3846 logger .debug ("ALSA using %s for rate conversion (requested 16kHz)" , dev )
3947 cmd = ["arecord" , "-D" , dev , "-f" , "S16_LE" , "-r" , str (SAMPLE_RATE ), "-c" , str (CHANNELS ), "-t" , "raw" ]
40- logger .info ("ALSA capture starting: %s (device=%s)" , " " .join (cmd ), device )
41- try :
42- proc = subprocess .Popen (
43- cmd ,
44- stdout = subprocess .PIPE ,
45- stderr = subprocess .PIPE ,
46- bufsize = CHUNK_BYTES ,
47- )
48- if proc_holder is not None :
49- proc_holder .append (proc )
50- except FileNotFoundError :
51- logger .warning ("arecord not found; cannot capture from ALSA device %s" , device )
52- out_queue .put (None )
53- return
54- except Exception as e :
55- logger .warning ("Failed to start arecord for %s: %s" , device , e )
56- out_queue .put (None )
57- return
58- first_chunk = True
59- try :
60- while not stop_event .is_set () and proc .poll () is None :
61- chunk = proc .stdout .read (CHUNK_BYTES )
62- if not chunk :
63- try :
64- err = proc .stderr .read ().decode ("utf-8" , errors = "replace" ).strip () if proc .stderr else ""
65- if err :
66- logger .warning ("ALSA capture read empty (device %s). arecord stderr: %s" , device , err )
67- else :
68- logger .warning ("ALSA capture read returned empty (device %s); check device/sample rate" , device )
69- except Exception :
70- logger .warning ("ALSA capture read returned empty (device %s)" , device )
71- break
72- if first_chunk :
73- first_chunk = False
74- logger .info ("ALSA first PCM chunk received from %s (%d bytes); pipeline will get amplitude" , device , len (chunk ))
75- out_queue .put (chunk )
76- except Exception as e :
77- logger .warning ("ALSA capture read error for %s: %s" , device , e )
78- finally :
48+
49+ retries = 0
50+ ever_produced_chunk = False
51+
52+ while not stop_event .is_set ():
53+ logger .info ("ALSA capture starting: %s (device=%s)" , " " .join (cmd ), device )
7954 try :
80- proc .terminate ()
81- proc .wait (timeout = 1 )
82- except Exception :
83- pass
84- out_queue .put (None )
85- if proc_holder is not None and proc_holder and proc_holder [0 ] is proc :
55+ proc = subprocess .Popen (
56+ cmd ,
57+ stdout = subprocess .PIPE ,
58+ stderr = subprocess .PIPE ,
59+ bufsize = CHUNK_BYTES ,
60+ )
61+ if proc_holder is not None :
62+ if proc_holder :
63+ proc_holder .clear ()
64+ proc_holder .append (proc )
65+ except FileNotFoundError :
66+ logger .warning ("arecord not found; cannot capture from ALSA device %s" , device )
67+ out_queue .put (None )
68+ return
69+ except Exception as e :
70+ logger .warning ("Failed to start arecord for %s: %s" , device , e )
71+ if retries >= MAX_CAPTURE_RETRIES :
72+ logger .error ("ALSA capture giving up after %d retries for %s" , retries , device )
73+ out_queue .put (None )
74+ return
75+ retries += 1
76+ delay = min (RETRY_BACKOFF_BASE * (2 ** (retries - 1 )), RETRY_BACKOFF_MAX )
77+ logger .info ("ALSA capture retry %d/%d in %.1fs for %s" , retries , MAX_CAPTURE_RETRIES , delay , device )
78+ stop_event .wait (delay )
79+ continue
80+
81+ first_chunk_this_run = True
82+ died_unexpectedly = False
83+ try :
84+ while not stop_event .is_set () and proc .poll () is None :
85+ chunk = proc .stdout .read (CHUNK_BYTES )
86+ if not chunk :
87+ try :
88+ err = proc .stderr .read ().decode ("utf-8" , errors = "replace" ).strip () if proc .stderr else ""
89+ if err :
90+ logger .warning ("ALSA capture read empty (device %s). arecord stderr: %s" , device , err )
91+ else :
92+ logger .warning ("ALSA capture read returned empty (device %s); check device/sample rate" , device )
93+ except Exception :
94+ logger .warning ("ALSA capture read returned empty (device %s)" , device )
95+ died_unexpectedly = True
96+ break
97+ if first_chunk_this_run :
98+ first_chunk_this_run = False
99+ if not ever_produced_chunk :
100+ logger .info ("ALSA first PCM chunk received from %s (%d bytes); pipeline will get amplitude" , device , len (chunk ))
101+ else :
102+ logger .info ("ALSA capture resumed from %s (%d bytes) after retry" , device , len (chunk ))
103+ retries = 0
104+ ever_produced_chunk = True
105+ out_queue .put (chunk )
106+ except Exception as e :
107+ logger .warning ("ALSA capture read error for %s: %s" , device , e )
108+ died_unexpectedly = True
109+ finally :
86110 try :
87- proc_holder .clear ()
111+ proc .terminate ()
112+ proc .wait (timeout = 1 )
88113 except Exception :
89114 pass
90- if first_chunk :
115+ if proc_holder is not None and proc_holder and proc_holder [0 ] is proc :
116+ try :
117+ proc_holder .clear ()
118+ except Exception :
119+ pass
120+
121+ if stop_event .is_set ():
122+ break
123+
124+ if died_unexpectedly and retries < MAX_CAPTURE_RETRIES :
125+ retries += 1
126+ delay = min (RETRY_BACKOFF_BASE * (2 ** (retries - 1 )), RETRY_BACKOFF_MAX )
127+ logger .warning (
128+ "ALSA capture died unexpectedly for %s; retry %d/%d in %.1fs" ,
129+ device , retries , MAX_CAPTURE_RETRIES , delay ,
130+ )
131+ stop_event .wait (delay )
132+ continue
133+
134+ if died_unexpectedly :
135+ logger .error ("ALSA capture giving up after %d retries for %s" , retries , device )
136+ elif first_chunk_this_run and not ever_produced_chunk :
91137 try :
92138 err = proc .stderr .read ().decode ("utf-8" , errors = "replace" ).strip () if proc .stderr else ""
93139 if err :
@@ -96,6 +142,9 @@ def _capture_alsa(
96142 logger .warning ("ALSA capture ended without sending any chunks (device %s); check arecord -D %s" , device , dev )
97143 except Exception :
98144 logger .warning ("ALSA capture ended without sending any chunks (device %s)" , device )
145+ break
146+
147+ out_queue .put (None )
99148
100149
101150def _capture_pyaudio (
0 commit comments