""" Microphone input monitor — standalone, no internal dependencies. Gracefully degrades if sounddevice/numpy are unavailable. .. deprecated:: For pipeline integration, use :class:`engine.sensors.mic.MicSensor` instead. MicSensor is a self-contained implementation and does not use MicMonitor. """ import atexit from collections.abc import Callable from datetime import datetime try: import numpy as _np import sounddevice as _sd _HAS_MIC = True except Exception: _HAS_MIC = False from engine.events import MicLevelEvent class MicMonitor: """Background mic stream that exposes current RMS dB level. .. deprecated:: For pipeline integration, use :class:`engine.sensors.mic.MicSensor` instead. """ def __init__(self, threshold_db=50): self.threshold_db = threshold_db self._db = -99.0 self._stream = None self._subscribers: list[Callable[[MicLevelEvent], None]] = [] @property def available(self): """True if sounddevice is importable.""" return _HAS_MIC @property def db(self): """Current RMS dB level.""" return self._db @property def excess(self): """dB above threshold (clamped to 0).""" return max(0.0, self._db - self.threshold_db) def subscribe(self, callback: Callable[[MicLevelEvent], None]) -> None: """Register a callback to be called when mic level changes.""" self._subscribers.append(callback) def unsubscribe(self, callback: Callable[[MicLevelEvent], None]) -> None: """Remove a registered callback.""" if callback in self._subscribers: self._subscribers.remove(callback) def _emit(self, event: MicLevelEvent) -> None: """Emit an event to all subscribers.""" for cb in self._subscribers: try: cb(event) except Exception: pass def start(self): """Start background mic stream. Returns True on success, False/None otherwise.""" if not _HAS_MIC: return None def _cb(indata, frames, t, status): rms = float(_np.sqrt(_np.mean(indata**2))) self._db = 20 * _np.log10(rms) if rms > 0 else -99.0 if self._subscribers: event = MicLevelEvent( db_level=self._db, excess_above_threshold=max(0.0, self._db - self.threshold_db), timestamp=datetime.now(), ) self._emit(event) try: self._stream = _sd.InputStream( callback=_cb, channels=1, samplerate=44100, blocksize=2048 ) self._stream.start() atexit.register(self.stop) return True except Exception: return False def stop(self): """Stop the mic stream if running.""" if self._stream: try: self._stream.stop() except Exception: pass self._stream = None