import logging from typing import Protocol import numpy as np from ..config import RuntimeConfig logger = logging.getLogger(__name__) class AudioListener(Protocol): def on_audio_data(self, audio_data: np.ndarray) -> None: ... class MicrophoneListener: def __init__(self, config: RuntimeConfig) -> None: self.config = config self._stream = None self._running = False self._listeners: list[AudioListener] = [] self._sample_rate = self.config.audio.sample_rate self._channels = self.config.audio.channels self._device = self.config.audio.input_device self._block_duration_ms = 30 self._block_size = int(self._sample_rate * (self._block_duration_ms / 1000)) def add_audio_listener(self, listener: AudioListener) -> None: if listener not in self._listeners: self._listeners.append(listener) def remove_audio_listener(self, listener: AudioListener) -> None: if listener in self._listeners: self._listeners.remove(listener) def start(self) -> None: try: import sounddevice as sd except ImportError as exc: raise RuntimeError( "Missing dependency: sounddevice. Install runtime dependencies before starting microphone listener." ) from exc self._stream = sd.InputStream( device=self._device, samplerate=self._sample_rate, channels=self._channels, dtype="int16", blocksize=self._block_size, callback=self._input_callback, latency="low", ) self._stream.start() self._running = True logger.info("microphone listener started") logger.info("microphone sample rate: %s", self._sample_rate) logger.info("microphone channels: %s", self._channels) logger.info("microphone device: %s", self._device if self._device is not None else "default") logger.info("microphone block size: %s", self._block_size) def stop(self) -> None: if not self._running and self._stream is None: return self._running = False if self._stream is not None: try: self._stream.stop() finally: self._stream.close() self._stream = None logger.info("microphone listener stopped") def _input_callback(self, indata, frames, time_info, status) -> None: _ = frames, time_info if status and "overflow" not in str(status).lower(): logger.warning("microphone status: %s", status) audio = np.copy(indata) if audio.ndim > 1: audio = audio[:, 0] else: audio = audio.reshape(-1) for listener in list(self._listeners): try: listener.on_audio_data(audio) except Exception: logger.exception("audio listener callback failed")