189 lines
7.1 KiB
Python
189 lines
7.1 KiB
Python
# 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
|