Files
whisper-local/whisper_local/microphone/_win32.py
T

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