Update __main__.py

(Major change) main() function completely refactored into helpers. _init_controllers now handles decoupled initialization. _process_audio loop logic was fixed to correctly initialize wake words on startup.

 mic.recorder() loop is now much more robust against OS buffering issue when the microphone is muted and wakewords were detected and processed after unmute.
This commit is contained in:
imonlinux
2025-11-17 22:40:53 -08:00
committed by GitHub
parent ada83bdfbb
commit fd5cd13f72
+78 -52
View File
@@ -134,7 +134,7 @@ async def main() -> None:
finally: finally:
# --- 10. Cleanup --- # --- 10. Cleanup ---
_LOGGER.debug("Shutting down...") _LOGGER.debug("Shutting down...")
state.mic_muted = True # Signal audio thread to stop state.shutdown = True # Signal audio thread to stop
audio_thread.join() audio_thread.join()
if hasattr(state, "mqtt_controller") and state.mqtt_controller: if hasattr(state, "mqtt_controller") and state.mqtt_controller:
@@ -404,69 +404,95 @@ def _process_audio(state: ServerState, mic, block_size: int):
last_active: Optional[float] = None last_active: Optional[float] = None
try: try:
_LOGGER.debug("Opening audio input device: %s", mic.name) while True:
with mic.recorder(samplerate=16000, channels=1, blocksize=block_size) as mic_in: # --- THIS IS THE FIX ---
while True: # Wait while muted. This loop will spin until unmuted.
if state.mic_muted: if state.mic_muted:
break _LOGGER.debug("Audio thread muted.")
while state.mic_muted:
if state.shutdown:
_LOGGER.debug("Shutdown signal received, stopping audio thread.")
return # Exit thread
time.sleep(0.1)
audio_chunk_array = mic_in.record(block_size).reshape(-1) # --- Just unmuted ---
audio_chunk = ( _LOGGER.debug("Audio thread unmuted, clearing buffers.")
(np.clip(audio_chunk_array, -1.0, 1.0) * 32767.0) micro_features.reset()
.astype("<i2") if oww_features is not None:
.tobytes() oww_features.reset()
) last_active = time.monotonic() # Start refractory period
if state.satellite is None: # Re-open microphone to flush OS buffer
time.sleep(0.01) _LOGGER.debug("Re-opening microphone to flush OS buffers...")
continue with mic.recorder(samplerate=16000, channels=1, blocksize=block_size) as mic_in:
mic_in.flush() # Discard any stale data
_LOGGER.debug("Microphone flushed.")
# Fall through to the main recording loop
# --- END FIX ---
# --- THIS IS THE FIX --- _LOGGER.debug("Opening audio input device: %s", mic.name)
if (not wake_words) or (state.wake_words_changed): with mic.recorder(samplerate=16000, channels=1, blocksize=block_size) as mic_in:
# --- END FIX --- # --- This is now the main recording loop ---
state.wake_words_changed = False while not state.mic_muted:
wake_words = [ if state.shutdown:
ww for ww in state.wake_words.values() _LOGGER.debug("Shutdown signal received, stopping audio thread.")
if ww.id in state.active_wake_words return # Exit thread
]
has_oww = any(isinstance(ww, OpenWakeWord) for ww in wake_words)
if has_oww and (oww_features is None): audio_chunk_array = mic_in.record(block_size).reshape(-1)
_LOGGER.debug("Initializing OpenWakeWord features...") audio_chunk = (
oww_features = OpenWakeWordFeatures.from_builtin() (np.clip(audio_chunk_array, -1.0, 1.0) * 32767.0)
.astype("<i2")
.tobytes()
)
try: if state.satellite is None:
micro_inputs.clear(); micro_inputs.extend(micro_features.process_streaming(audio_chunk)) time.sleep(0.01)
continue
if has_oww: if (not wake_words) or (state.wake_words_changed):
assert oww_features is not None state.wake_words_changed = False
oww_inputs.clear(); oww_inputs.extend(oww_features.process_streaming(audio_chunk)) wake_words = [
ww for ww in state.wake_words.values()
if ww.id in state.active_wake_words
]
has_oww = any(isinstance(ww, OpenWakeWord) for ww in wake_words)
for wake_word in wake_words: if has_oww and (oww_features is None):
activated = False _LOGGER.debug("Initializing OpenWakeWord features...")
if isinstance(wake_word, MicroWakeWord): oww_features = OpenWakeWordFeatures.from_builtin()
if any(wake_word.process_streaming(mi) for mi in micro_inputs): activated = True
elif isinstance(wake_word, OpenWakeWord):
if any(p > 0.5 for oi in oww_inputs for p in wake_word.process_streaming(oi)): activated = True
if activated: try:
now = time.monotonic() micro_inputs.clear(); micro_inputs.extend(micro_features.process_streaming(audio_chunk))
if (last_active is None) or ((now - last_active) > state.refractory_seconds):
state.loop.call_soon_threadsafe(state.satellite.wakeup, wake_word)
last_active = now
stopped = False if has_oww:
for micro_input in micro_inputs: assert oww_features is not None
if state.stop_word.process_streaming(micro_input): oww_inputs.clear(); oww_inputs.extend(oww_features.process_streaming(audio_chunk))
stopped = True
if stopped and (state.stop_word.id in state.active_wake_words): for wake_word in wake_words:
state.loop.call_soon_threadsafe(state.satellite.stop) activated = False
if isinstance(wake_word, MicroWakeWord):
if any(wake_word.process_streaming(mi) for mi in micro_inputs): activated = True
elif isinstance(wake_word, OpenWakeWord):
if any(p > 0.5 for oi in oww_inputs for p in wake_word.process_streaming(oi)): activated = True
state.loop.call_soon_threadsafe(state.satellite.handle_audio, audio_chunk) if activated:
now = time.monotonic()
if (last_active is None) or ((now - last_active) > state.refractory_seconds):
state.loop.call_soon_threadsafe(state.satellite.wakeup, wake_word)
last_active = now
except Exception: stopped = False
_LOGGER.exception("Unexpected error handling audio") for micro_input in micro_inputs:
if state.stop_word.process_streaming(micro_input):
stopped = True
if stopped and (state.stop_word.id in state.active_wake_words):
state.loop.call_soon_threadsafe(state.satellite.stop)
state.loop.call_soon_threadsafe(state.satellite.handle_audio, audio_chunk)
except Exception:
_LOGGER.exception("Unexpected error handling audio")
except Exception as e: except Exception as e:
_LOGGER.critical("A soundcard error occurred: %s", e) _LOGGER.critical("A soundcard error occurred: %s", e)