# whisper_local/microphone/_win32.py """Windows Mikrofon-Monitor via IMMNotificationClient (Core Audio API).""" import asyncio import ctypes import logging from collections.abc import Awaitable, Callable import sounddevice as sd logger = logging.getLogger(__name__) _CLSID_MMDeviceEnumerator = "{BCDE0395-E52F-467C-8E3D-C4579291692E}" _IID_IMMDeviceEnumerator = "{A95664D2-9614-4F35-A746-DE8DB63617E6}" _IID_IMMNotificationClient = "{7991EEC9-7E89-4D85-8390-6C703CEC60C0}" def _build_com_interfaces(): """Definiert IMMDeviceEnumerator und IMMNotificationClient via comtypes.""" import comtypes from comtypes import COMMETHOD, GUID, HRESULT, IUnknown, POINTER class _IMMNotificationClient(IUnknown): _iid_ = GUID(_IID_IMMNotificationClient) _methods_ = [ COMMETHOD([], HRESULT, "OnDeviceStateChanged", (["in"], ctypes.c_wchar_p, "pwstrDeviceId"), (["in"], ctypes.c_uint, "dwNewState")), COMMETHOD([], HRESULT, "OnDeviceAdded", (["in"], ctypes.c_wchar_p, "pwstrDeviceId")), COMMETHOD([], HRESULT, "OnDeviceRemoved", (["in"], ctypes.c_wchar_p, "pwstrDeviceId")), COMMETHOD([], HRESULT, "OnDefaultDeviceChanged", (["in"], ctypes.c_int, "flow"), (["in"], ctypes.c_int, "role"), (["in"], ctypes.c_wchar_p, "pwstrDefaultDeviceId")), COMMETHOD([], HRESULT, "OnPropertyValueChanged", (["in"], ctypes.c_wchar_p, "pwstrDeviceId"), (["in"], ctypes.c_void_p, "key")), ] class _IMMDeviceEnumerator(IUnknown): _iid_ = GUID(_IID_IMMDeviceEnumerator) _methods_ = [ COMMETHOD([], HRESULT, "EnumAudioEndpoints", (["in"], ctypes.c_int, "dataFlow"), (["in"], ctypes.c_uint, "dwStateMask"), (["out"], POINTER(IUnknown), "ppDevices")), COMMETHOD([], HRESULT, "GetDefaultAudioEndpoint", (["in"], ctypes.c_int, "dataFlow"), (["in"], ctypes.c_int, "role"), (["out"], POINTER(IUnknown), "ppEndpoint")), COMMETHOD([], HRESULT, "GetDevice", (["in"], ctypes.c_wchar_p, "pwstrId"), (["out"], POINTER(IUnknown), "ppDevice")), COMMETHOD([], HRESULT, "RegisterEndpointNotificationCallback", (["in"], POINTER(_IMMNotificationClient), "pClient")), COMMETHOD([], HRESULT, "UnregisterEndpointNotificationCallback", (["in"], POINTER(_IMMNotificationClient), "pClient")), ] return _IMMNotificationClient, _IMMDeviceEnumerator def _build_client_class(IMMNotificationClient, callback): """Erstellt eine comtypes.COMObject-Implementierung von IMMNotificationClient.""" import comtypes class _NotificationClientImpl(comtypes.COMObject): _com_interfaces_ = [IMMNotificationClient] def OnDeviceStateChanged(self, pwstrDeviceId, dwNewState): callback() return 0 def OnDeviceAdded(self, pwstrDeviceId): callback() return 0 def OnDeviceRemoved(self, pwstrDeviceId): callback() return 0 def OnDefaultDeviceChanged(self, flow, role, pwstrDefaultDeviceId): return 0 def OnPropertyValueChanged(self, pwstrDeviceId, key): return 0 return _NotificationClientImpl() class Win32Monitor: def __init__(self, configured_device: str | None): self.configured_device = configured_device self.on_device_added: Callable[[str], Awaitable[None]] | None = None self.on_device_removed: Callable[[str], Awaitable[None]] | None = None self.on_configured_missing: Callable[[], Awaitable[None]] | None = None self._loop: asyncio.AbstractEventLoop | None = None self._known_devices: set[str] = set() self._enumerator = None self._client = None self._fallback = None def _current_devices(self) -> set[str]: try: return { dev["name"] for dev in sd.query_devices() if dev["max_input_channels"] > 0 } except Exception: logger.exception("Fehler beim Abfragen der Audiogeräte") return self._known_devices.copy() async def start(self) -> None: self._loop = asyncio.get_running_loop() self._known_devices = self._current_devices() if ( self.configured_device and self.configured_device not in self._known_devices and self.on_configured_missing ): await self.on_configured_missing() try: self._start_com() except Exception: logger.warning( "IMMNotificationClient nicht verfügbar, Fallback auf Polling", exc_info=True, ) from whisper_local.microphone._poll import PollMonitor fallback = PollMonitor(self.configured_device) fallback.on_device_added = self.on_device_added fallback.on_device_removed = self.on_device_removed fallback._known_devices = self._known_devices self._fallback = fallback self._fallback._task = asyncio.create_task(self._fallback._loop()) def _start_com(self) -> None: import comtypes import comtypes.client from comtypes import GUID comtypes.CoInitialize() IMMNotificationClient, IMMDeviceEnumerator = _build_com_interfaces() self._enumerator = comtypes.client.CreateObject( GUID(_CLSID_MMDeviceEnumerator), interface=IMMDeviceEnumerator, ) self._client = _build_client_class(IMMNotificationClient, self._on_com_event) self._enumerator.RegisterEndpointNotificationCallback(self._client) def _on_com_event(self) -> None: if self._loop is not None: self._loop.call_soon_threadsafe( lambda: asyncio.ensure_future(self._handle_change()) ) async def _handle_change(self) -> None: current = self._current_devices() added = current - self._known_devices removed = self._known_devices - current self._known_devices = current for name in added: if self.on_device_added: await self.on_device_added(name) for name in removed: if self.on_device_removed: await self.on_device_removed(name) def stop(self) -> None: if self._fallback is not None: self._fallback.stop() return if self._enumerator is not None and self._client is not None: try: self._enumerator.UnregisterEndpointNotificationCallback(self._client) except Exception: logger.warning("Fehler beim Deregistrieren des Notification-Clients") try: import comtypes comtypes.CoUninitialize() except Exception: pass