# Mikrofon-Monitor – Design-Spec **Datum:** 2026-05-14 **Status:** Genehmigt ## Ziel Die Anwendung soll erkennen, wenn Mikrofone im System hinzugefügt oder entfernt werden (USB, Bluetooth). Wenn das konfigurierte Mikrofon nicht mehr vorhanden ist, fällt die App automatisch auf das Standard-Mikrofon zurück und benachrichtigt den Nutzer. ## Verhalten - **Konfiguriertes Mikrofon verschwindet:** Automatischer Fallback auf Standard-Mikrofon (`device = None`), Toast-Benachrichtigung + Tray-Tooltip-Warnung. - **Konfiguriertes Mikrofon kehrt zurück:** Stellt das konfigurierte Gerät wieder her, Toast „Mikrofon wieder verbunden", Tray-Tooltip-Warnung entfernen. - **Konfiguriertes Mikrofon fehlt bereits beim Start:** `on_configured_missing` sofort auslösen, nicht erst nach dem ersten Poll-Intervall. - **Kein konfiguriertes Mikrofon (`device = None`):** Monitor läuft trotzdem, meldet nur `on_device_added` / `on_device_removed` (für zukünftige Erweiterungen), keine Fehlerbehandlung nötig. ## Neue Dateien ``` whisper_local/ microphone/ __init__.py # Protocol + create_monitor() Factory _poll.py # Polling-Implementierung (Linux + Fallback) _win32.py # IMMNotificationClient-Implementierung (Windows) tray/ _notification.py # notify-py Toast-Wrapper ``` ## Protocol ```python class MicrophoneMonitor(Protocol): on_device_added: Callable[[str], Awaitable[None]] | None on_device_removed: Callable[[str], Awaitable[None]] | None on_configured_missing: Callable[[], Awaitable[None]] | None async def start(self) -> None: ... def stop(self) -> None: ... ``` ## Factory ```python def create_monitor(configured_device: str | None) -> MicrophoneMonitor: ... ``` - Windows: versucht `Win32Monitor`; fällt bei COM-Fehler auf `PollMonitor` zurück. - Alle anderen Plattformen: `PollMonitor`. ## Erkennungsmechanismus ### Windows: `Win32Monitor` (`_win32.py`) - Registriert `IMMNotificationClient` bei `IMMDeviceEnumerator` via `comtypes`. - Empfängt `OnDeviceAdded`, `OnDeviceRemoved`, `OnDeviceStateChanged` als COM-Callbacks. - Übergibt Events per `loop.call_soon_threadsafe` in den asyncio-Loop (identisches Muster wie pynput-Hotkey-Callbacks). - Wenn COM-Initialisierung fehlschlägt: transparenter Fallback auf `PollMonitor`. ### Cross-Platform: `PollMonitor` (`_poll.py`) - Asyncio-Task mit `asyncio.sleep(2.5)` zwischen Prüfungen. - Vergleicht aktuelle `sd.query_devices()`-Liste mit letztem Snapshot (Set aus Gerätenamen). - Erkennt hinzugekommene und verschwundene Geräte, löst entsprechende Callbacks aus. - Exceptions in `sd.query_devices()` werden geloggt und übersprungen — Loop läuft weiter. ## Benachrichtigung (`_notification.py`) - Nutzt `notify-py` (cross-platform, aktiv gepflegt). - Funktion `notify(title: str, message: str) -> None` — bei Fehler wird geloggt, nie blockierend. - Neue Abhängigkeit: `notify-py` in `pyproject.toml` eintragen. ## Tray-Integration `PystrayApp` erhält neue Methode: ```python def set_warning(self, msg: str | None) -> None: ... ``` - `msg = None`: Icon-Titel zurück auf `"whisper-local"`. - `msg = "..."`: Icon-Titel auf `"whisper-local ⚠ "`. ## App-Integration (`__main__.py`) - `App.__init__`: erstellt `self.monitor = create_monitor(config.microphone)`. - Callbacks registrieren: - `on_configured_missing` → `recorder.device = None` + `notify(...)` + `tray.set_warning(...)` - `on_device_added` → prüft ob konfiguriertes Gerät zurückgekehrt → `recorder.device = config.microphone` + `notify(...)` + `tray.set_warning(None)` - `App.run()`: startet `monitor.start()` als asyncio-Task parallel zum Hotkey-Listener. - `_on_config_reload`: stoppt alten Monitor, erstellt neuen mit aktualisierten Gerätedaten. ## Fehlerbehandlung | Situation | Verhalten | |-----------|-----------| | COM-Init schlägt fehl (Windows) | Transparenter Fallback auf `PollMonitor` | | `sd.query_devices()` wirft Exception | Loggen + überspringen, Loop läuft weiter | | `notify-py` wirft Exception | Loggen + ignorieren, nie blockierend | | Konfiguriertes Mikrofon fehlt beim Start | Sofort `on_configured_missing` auslösen | ## Tests - `tests/test_microphone_monitor.py`: - `PollMonitor` mit gemocktem `sd.query_devices()`. - Prüft: `on_device_added` wird ausgelöst wenn Gerät erscheint. - Prüft: `on_device_removed` wird ausgelöst wenn Gerät verschwindet. - Prüft: `on_configured_missing` wird sofort beim Start ausgelöst wenn Gerät fehlt. - `Win32Monitor`-Tests nur auf Windows (`@pytest.mark.skipif(sys.platform != "win32", ...)`), COM-Interface gemockt. - `_notification.py`: kein eigener Test (trivialer Wrapper). ## Abhängigkeiten | Paket | Zweck | Plattform | |-------|-------|-----------| | `notify-py` | Desktop-Toast-Benachrichtigungen | alle | | `comtypes` | COM-Interface für IMMNotificationClient | Windows (bereits transitiv vorhanden via pywinrt) |