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:
@@ -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:
|
||||||
audio_chunk_array = mic_in.record(block_size).reshape(-1)
|
if state.shutdown:
|
||||||
audio_chunk = (
|
_LOGGER.debug("Shutdown signal received, stopping audio thread.")
|
||||||
(np.clip(audio_chunk_array, -1.0, 1.0) * 32767.0)
|
return # Exit thread
|
||||||
.astype("<i2")
|
time.sleep(0.1)
|
||||||
.tobytes()
|
|
||||||
)
|
|
||||||
|
|
||||||
if state.satellite is None:
|
# --- Just unmuted ---
|
||||||
time.sleep(0.01)
|
_LOGGER.debug("Audio thread unmuted, clearing buffers.")
|
||||||
continue
|
micro_features.reset()
|
||||||
|
if oww_features is not None:
|
||||||
|
oww_features.reset()
|
||||||
|
last_active = time.monotonic() # Start refractory period
|
||||||
|
|
||||||
# --- THIS IS THE FIX ---
|
# Re-open microphone to flush OS buffer
|
||||||
if (not wake_words) or (state.wake_words_changed):
|
_LOGGER.debug("Re-opening microphone to flush OS buffers...")
|
||||||
# --- END FIX ---
|
with mic.recorder(samplerate=16000, channels=1, blocksize=block_size) as mic_in:
|
||||||
state.wake_words_changed = False
|
mic_in.flush() # Discard any stale data
|
||||||
wake_words = [
|
_LOGGER.debug("Microphone flushed.")
|
||||||
ww for ww in state.wake_words.values()
|
# Fall through to the main recording loop
|
||||||
if ww.id in state.active_wake_words
|
# --- END FIX ---
|
||||||
]
|
|
||||||
has_oww = any(isinstance(ww, OpenWakeWord) for ww in wake_words)
|
_LOGGER.debug("Opening audio input device: %s", mic.name)
|
||||||
|
with mic.recorder(samplerate=16000, channels=1, blocksize=block_size) as mic_in:
|
||||||
|
# --- This is now the main recording loop ---
|
||||||
|
while not state.mic_muted:
|
||||||
|
if state.shutdown:
|
||||||
|
_LOGGER.debug("Shutdown signal received, stopping audio thread.")
|
||||||
|
return # Exit thread
|
||||||
|
|
||||||
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")
|
||||||
try:
|
.tobytes()
|
||||||
micro_inputs.clear(); micro_inputs.extend(micro_features.process_streaming(audio_chunk))
|
)
|
||||||
|
|
||||||
|
if state.satellite is None:
|
||||||
|
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()
|
||||||
for wake_word in wake_words:
|
if ww.id in state.active_wake_words
|
||||||
activated = False
|
]
|
||||||
if isinstance(wake_word, MicroWakeWord):
|
has_oww = any(isinstance(ww, OpenWakeWord) for ww in wake_words)
|
||||||
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:
|
if has_oww and (oww_features is None):
|
||||||
now = time.monotonic()
|
_LOGGER.debug("Initializing OpenWakeWord features...")
|
||||||
if (last_active is None) or ((now - last_active) > state.refractory_seconds):
|
oww_features = OpenWakeWordFeatures.from_builtin()
|
||||||
state.loop.call_soon_threadsafe(state.satellite.wakeup, wake_word)
|
|
||||||
last_active = now
|
|
||||||
|
|
||||||
stopped = False
|
try:
|
||||||
for micro_input in micro_inputs:
|
micro_inputs.clear(); micro_inputs.extend(micro_features.process_streaming(audio_chunk))
|
||||||
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)
|
if has_oww:
|
||||||
|
assert oww_features is not None
|
||||||
|
oww_inputs.clear(); oww_inputs.extend(oww_features.process_streaming(audio_chunk))
|
||||||
|
|
||||||
|
for wake_word in wake_words:
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
stopped = False
|
||||||
|
for micro_input in micro_inputs:
|
||||||
|
if state.stop_word.process_streaming(micro_input):
|
||||||
|
stopped = True
|
||||||
|
|
||||||
except Exception:
|
if stopped and (state.stop_word.id in state.active_wake_words):
|
||||||
_LOGGER.exception("Unexpected error handling audio")
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user