デバイス管理モジュールのインポートをガードし、Windows固有の依存関係をオプショナルに変更。クラスの初期化メソッドを修正し、デフォルトデバイス変更時のコールバックを追加。ドキュメントを新規作成し、使用例や注意点を明示化。エラーハンドリングを強化し、コードの可読性を向上。
This commit is contained in:
@@ -1,27 +1,53 @@
|
|||||||
from typing import Callable
|
from typing import Callable, Dict, List, Optional, Any
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import comtypes
|
|
||||||
from pyaudiowpatch import PyAudio, paWASAPI
|
# Optional, Windows-specific dependencies. Guard imports so module can be imported on non-Windows systems.
|
||||||
from pycaw.callbacks import MMNotificationClient
|
try:
|
||||||
from pycaw.utils import AudioUtilities
|
import comtypes
|
||||||
|
except Exception: # pragma: no cover - optional runtime
|
||||||
|
comtypes = None # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyaudiowpatch import PyAudio, paWASAPI
|
||||||
|
except Exception: # pragma: no cover - optional runtime
|
||||||
|
PyAudio = None # type: ignore
|
||||||
|
paWASAPI = None # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pycaw.callbacks import MMNotificationClient
|
||||||
|
from pycaw.utils import AudioUtilities
|
||||||
|
except Exception: # pragma: no cover - optional runtime
|
||||||
|
MMNotificationClient = object # type: ignore
|
||||||
|
AudioUtilities = None # type: ignore
|
||||||
|
|
||||||
from utils import errorLogging
|
from utils import errorLogging
|
||||||
|
|
||||||
class Client(MMNotificationClient):
|
class Client(MMNotificationClient):
|
||||||
def __init__(self):
|
"""Callback client used by pycaw to detect device changes.
|
||||||
super().__init__()
|
|
||||||
self.loop = True
|
|
||||||
|
|
||||||
def on_default_device_changed(self, flow, flow_id, role, role_id, default_device_id):
|
This subclass is lightweight: it flips a flag when events arrive so the
|
||||||
|
monitoring loop can break and refresh device lists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# If MMNotificationClient is the placeholder object (non-windows), avoid calling super
|
||||||
|
try:
|
||||||
|
super().__init__()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.loop: bool = True
|
||||||
|
|
||||||
|
def on_default_device_changed(self, *args: Any, **kwargs: Any) -> None:
|
||||||
self.loop = False
|
self.loop = False
|
||||||
|
|
||||||
def on_device_added(self, added_device_id):
|
def on_device_added(self, *args: Any, **kwargs: Any) -> None:
|
||||||
self.loop = False
|
self.loop = False
|
||||||
|
|
||||||
def on_device_removed(self, removed_device_id):
|
def on_device_removed(self, *args: Any, **kwargs: Any) -> None:
|
||||||
self.loop = False
|
self.loop = False
|
||||||
|
|
||||||
def on_device_state_changed(self, device_id, state):
|
def on_device_state_changed(self, *args: Any, **kwargs: Any) -> None:
|
||||||
self.loop = False
|
self.loop = False
|
||||||
|
|
||||||
# def on_property_value_changed(self, device_id, key):
|
# def on_property_value_changed(self, device_id, key):
|
||||||
@@ -33,108 +59,150 @@ class DeviceManager:
|
|||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super(DeviceManager, cls).__new__(cls)
|
cls._instance = super(DeviceManager, cls).__new__(cls)
|
||||||
cls._instance.init()
|
# do NOT auto-init monitoring-heavy resources on import; require explicit init
|
||||||
|
cls._instance._initialized = False
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def init(self):
|
def init(self) -> None:
|
||||||
self.mic_devices = {"NoHost": [{"index": -1, "name": "NoDevice"}]}
|
"""Initialize internal state. This is intentionally separate from object
|
||||||
self.default_mic_device = {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}}
|
creation so importing the module won't start threads or access OS
|
||||||
self.speaker_devices = [{"index": -1, "name": "NoDevice"}]
|
audio APIs. Call `device_manager.init()` and then
|
||||||
self.default_speaker_device = {"device": {"index": -1, "name": "NoDevice"}}
|
`device_manager.startMonitoring()` explicitly when ready.
|
||||||
|
"""
|
||||||
|
if getattr(self, "_initialized", False):
|
||||||
|
return
|
||||||
|
|
||||||
self.update()
|
self.mic_devices: Dict[str, List[Dict[str, Any]]] = {"NoHost": [{"index": -1, "name": "NoDevice"}]}
|
||||||
|
self.default_mic_device: Dict[str, Any] = {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}}
|
||||||
|
self.speaker_devices: List[Dict[str, Any]] = [{"index": -1, "name": "NoDevice"}]
|
||||||
|
self.default_speaker_device: Dict[str, Any] = {"device": {"index": -1, "name": "NoDevice"}}
|
||||||
|
|
||||||
self.prev_mic_host = [host for host in self.mic_devices]
|
# Initialize previous state trackers
|
||||||
self.prev_mic_devices = self.mic_devices
|
self.prev_mic_host: List[str] = [host for host in self.mic_devices]
|
||||||
self.prev_default_mic_device = self.default_mic_device
|
self.prev_mic_devices: Dict[str, List[Dict[str, Any]]] = self.mic_devices
|
||||||
self.prev_speaker_devices = self.speaker_devices
|
self.prev_default_mic_device: Dict[str, Any] = self.default_mic_device
|
||||||
self.prev_default_speaker_device = self.default_speaker_device
|
self.prev_speaker_devices: List[Dict[str, Any]] = self.speaker_devices
|
||||||
|
self.prev_default_speaker_device: Dict[str, Any] = self.default_speaker_device
|
||||||
|
|
||||||
self.update_flag_default_mic_device = False
|
# Update flags
|
||||||
self.update_flag_default_speaker_device = False
|
self.update_flag_default_mic_device: bool = False
|
||||||
self.update_flag_host_list = False
|
self.update_flag_default_speaker_device: bool = False
|
||||||
self.update_flag_mic_device_list = False
|
self.update_flag_host_list: bool = False
|
||||||
self.update_flag_speaker_device_list = False
|
self.update_flag_mic_device_list: bool = False
|
||||||
|
self.update_flag_speaker_device_list: bool = False
|
||||||
|
|
||||||
self.callback_default_mic_device = None
|
# Callbacks
|
||||||
self.callback_default_speaker_device = None
|
self.callback_default_mic_device: Optional[Callable[..., None]] = None
|
||||||
self.callback_host_list = None
|
self.callback_default_speaker_device: Optional[Callable[..., None]] = None
|
||||||
self.callback_mic_device_list = None
|
self.callback_host_list: Optional[Callable[..., None]] = None
|
||||||
self.callback_speaker_device_list = None
|
self.callback_mic_device_list: Optional[Callable[..., None]] = None
|
||||||
self.callback_process_before_update_devices = None
|
self.callback_speaker_device_list: Optional[Callable[..., None]] = None
|
||||||
self.callback_process_after_update_devices = None
|
self.callback_process_before_update_mic_devices: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_process_after_update_mic_devices: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_process_before_update_speaker_devices: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_process_after_update_speaker_devices: Optional[Callable[..., None]] = None
|
||||||
|
|
||||||
self.monitoring_flag = False
|
# Monitoring control
|
||||||
self.startMonitoring()
|
self.monitoring_flag: bool = False
|
||||||
|
self.th_monitoring: Optional[Thread] = None
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
buffer_mic_devices = {}
|
buffer_mic_devices: Dict[str, List[Dict[str, Any]]] = {}
|
||||||
buffer_default_mic_device = {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}}
|
buffer_default_mic_device: Dict[str, Any] = {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}}
|
||||||
buffer_speaker_devices = []
|
buffer_speaker_devices: List[Dict[str, Any]] = []
|
||||||
buffer_default_speaker_device = {"device": {"index": -1, "name": "NoDevice"}}
|
buffer_default_speaker_device: Dict[str, Any] = {"device": {"index": -1, "name": "NoDevice"}}
|
||||||
|
|
||||||
with PyAudio() as p:
|
if PyAudio is None:
|
||||||
for host_index in range(p.get_host_api_count()):
|
# PyAudio not available; leave defaults in place
|
||||||
host = p.get_host_api_info_by_index(host_index)
|
self.mic_devices = buffer_mic_devices or {"NoHost": [{"index": -1, "name": "NoDevice"}]}
|
||||||
device_count = host.get('deviceCount', 0)
|
self.default_mic_device = buffer_default_mic_device
|
||||||
for device_index in range(device_count):
|
self.speaker_devices = buffer_speaker_devices or [{"index": -1, "name": "NoDevice"}]
|
||||||
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
self.default_speaker_device = buffer_default_speaker_device
|
||||||
if device.get("maxInputChannels", 0) > 0 and not device.get("isLoopbackDevice", True):
|
return
|
||||||
buffer_mic_devices.setdefault(host["name"], []).append(device)
|
|
||||||
if not buffer_mic_devices:
|
|
||||||
buffer_mic_devices = {"NoHost": [{"index": -1, "name": "NoDevice"}]}
|
|
||||||
|
|
||||||
api_info = p.get_default_host_api_info()
|
try:
|
||||||
default_mic_device = api_info["defaultInputDevice"]
|
with PyAudio() as p:
|
||||||
|
# gather input devices grouped by host
|
||||||
for host_index in range(p.get_host_api_count()):
|
for host_index in range(p.get_host_api_count()):
|
||||||
host = p.get_host_api_info_by_index(host_index)
|
host = p.get_host_api_info_by_index(host_index)
|
||||||
device_count = host.get('deviceCount', 0)
|
|
||||||
for device_index in range(device_count):
|
|
||||||
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
|
||||||
if device["index"] == default_mic_device:
|
|
||||||
buffer_default_mic_device = {"host": host, "device": device}
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
|
|
||||||
speaker_devices = []
|
|
||||||
wasapi_info = p.get_host_api_info_by_type(paWASAPI)
|
|
||||||
wasapi_name = wasapi_info["name"]
|
|
||||||
for host_index in range(p.get_host_api_count()):
|
|
||||||
host = p.get_host_api_info_by_index(host_index)
|
|
||||||
if host["name"] == wasapi_name:
|
|
||||||
device_count = host.get('deviceCount', 0)
|
device_count = host.get('deviceCount', 0)
|
||||||
for device_index in range(device_count):
|
for device_index in range(device_count):
|
||||||
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
||||||
if not device.get("isLoopbackDevice", True):
|
if device.get("maxInputChannels", 0) > 0 and not device.get("isLoopbackDevice", True):
|
||||||
for loopback in p.get_loopback_device_info_generator():
|
buffer_mic_devices.setdefault(host["name"], []).append(device)
|
||||||
if device["name"] in loopback["name"]:
|
if not buffer_mic_devices:
|
||||||
speaker_devices.append(loopback)
|
buffer_mic_devices = {"NoHost": [{"index": -1, "name": "NoDevice"}]}
|
||||||
speaker_devices = [dict(t) for t in {tuple(d.items()) for d in speaker_devices}] or [{"index": -1, "name": "NoDevice"}]
|
|
||||||
buffer_speaker_devices = sorted(speaker_devices, key=lambda d: d['index'])
|
|
||||||
|
|
||||||
wasapi_info = p.get_host_api_info_by_type(paWASAPI)
|
api_info = p.get_default_host_api_info()
|
||||||
default_speaker_device_index = wasapi_info["defaultOutputDevice"]
|
default_mic_device = api_info.get("defaultInputDevice", -1)
|
||||||
|
|
||||||
for host_index in range(p.get_host_api_count()):
|
for host_index in range(p.get_host_api_count()):
|
||||||
host_info = p.get_host_api_info_by_index(host_index)
|
host = p.get_host_api_info_by_index(host_index)
|
||||||
device_count = host_info.get('deviceCount', 0)
|
device_count = host.get('deviceCount', 0)
|
||||||
for device_index in range(0, device_count):
|
for device_index in range(device_count):
|
||||||
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
||||||
if device["index"] == default_speaker_device_index:
|
if device.get("index") == default_mic_device:
|
||||||
default_speakers = device
|
buffer_default_mic_device = {"host": host, "device": device}
|
||||||
if not default_speakers.get("isLoopbackDevice", True):
|
break
|
||||||
for loopback in p.get_loopback_device_info_generator():
|
else:
|
||||||
if default_speakers["name"] in loopback["name"]:
|
continue
|
||||||
buffer_default_speaker_device = {"device": loopback}
|
|
||||||
break
|
|
||||||
break
|
|
||||||
|
|
||||||
if buffer_default_speaker_device["device"]["name"] != "NoDevice":
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# collect speaker loopback devices (requires WASAPI)
|
||||||
|
speaker_devices: List[Dict[str, Any]] = []
|
||||||
|
if paWASAPI is not None:
|
||||||
|
try:
|
||||||
|
wasapi_info = p.get_host_api_info_by_type(paWASAPI)
|
||||||
|
wasapi_name = wasapi_info.get("name")
|
||||||
|
for host_index in range(p.get_host_api_count()):
|
||||||
|
host = p.get_host_api_info_by_index(host_index)
|
||||||
|
if host.get("name") == wasapi_name:
|
||||||
|
device_count = host.get('deviceCount', 0)
|
||||||
|
for device_index in range(device_count):
|
||||||
|
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
||||||
|
if not device.get("isLoopbackDevice", True):
|
||||||
|
for loopback in p.get_loopback_device_info_generator():
|
||||||
|
# match by name inclusion
|
||||||
|
if device.get("name") in loopback.get("name", ""):
|
||||||
|
speaker_devices.append(loopback)
|
||||||
|
except Exception:
|
||||||
|
# WASAPI not available or failed; ignore and continue
|
||||||
|
pass
|
||||||
|
|
||||||
|
# deduplicate and sort
|
||||||
|
speaker_devices = [dict(t) for t in {tuple(d.items()) for d in speaker_devices}] or [{"index": -1, "name": "NoDevice"}]
|
||||||
|
buffer_speaker_devices = sorted(speaker_devices, key=lambda d: d.get('index', -1))
|
||||||
|
|
||||||
|
# default speaker
|
||||||
|
if paWASAPI is not None:
|
||||||
|
try:
|
||||||
|
wasapi_info = p.get_host_api_info_by_type(paWASAPI)
|
||||||
|
default_speaker_device_index = wasapi_info.get("defaultOutputDevice", -1)
|
||||||
|
for host_index in range(p.get_host_api_count()):
|
||||||
|
host_info = p.get_host_api_info_by_index(host_index)
|
||||||
|
device_count = host_info.get('deviceCount', 0)
|
||||||
|
for device_index in range(0, device_count):
|
||||||
|
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
||||||
|
if device.get("index") == default_speaker_device_index:
|
||||||
|
default_speakers = device
|
||||||
|
if not default_speakers.get("isLoopbackDevice", True):
|
||||||
|
for loopback in p.get_loopback_device_info_generator():
|
||||||
|
if default_speakers.get("name") in loopback.get("name", ""):
|
||||||
|
buffer_default_speaker_device = {"device": loopback}
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
|
if buffer_default_speaker_device["device"].get("name") != "NoDevice":
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
# best-effort; ignore failures
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
self.mic_devices = buffer_mic_devices
|
self.mic_devices = buffer_mic_devices
|
||||||
self.default_mic_device = buffer_default_mic_device
|
self.default_mic_device = buffer_default_mic_device
|
||||||
self.speaker_devices = buffer_speaker_devices
|
self.speaker_devices = buffer_speaker_devices
|
||||||
@@ -170,14 +238,27 @@ class DeviceManager:
|
|||||||
try:
|
try:
|
||||||
while self.monitoring_flag is True:
|
while self.monitoring_flag is True:
|
||||||
try:
|
try:
|
||||||
comtypes.CoInitialize()
|
# Use COM only when available (Windows). If comtypes is not present,
|
||||||
cb = Client()
|
# fall back to periodic polling using PyAudio only.
|
||||||
enumerator = AudioUtilities.GetDeviceEnumerator()
|
if comtypes is not None and AudioUtilities is not None:
|
||||||
enumerator.RegisterEndpointNotificationCallback(cb)
|
try:
|
||||||
while cb.loop is True:
|
comtypes.CoInitialize()
|
||||||
sleep(1)
|
cb = Client()
|
||||||
enumerator.UnregisterEndpointNotificationCallback(cb)
|
enumerator = AudioUtilities.GetDeviceEnumerator()
|
||||||
comtypes.CoUninitialize()
|
enumerator.RegisterEndpointNotificationCallback(cb)
|
||||||
|
while cb.loop is True and self.monitoring_flag is True:
|
||||||
|
sleep(1)
|
||||||
|
try:
|
||||||
|
enumerator.UnregisterEndpointNotificationCallback(cb)
|
||||||
|
except Exception:
|
||||||
|
# best-effort unregister
|
||||||
|
pass
|
||||||
|
comtypes.CoUninitialize()
|
||||||
|
except Exception:
|
||||||
|
# if COM monitoring fails, log and fall through to polling
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
# polling and update cycle
|
||||||
self.runProcessBeforeUpdateMicDevices()
|
self.runProcessBeforeUpdateMicDevices()
|
||||||
self.runProcessBeforeUpdateSpeakerDevices()
|
self.runProcessBeforeUpdateSpeakerDevices()
|
||||||
sleep(2)
|
sleep(2)
|
||||||
@@ -191,12 +272,12 @@ class DeviceManager:
|
|||||||
self.runProcessAfterUpdateSpeakerDevices()
|
self.runProcessAfterUpdateSpeakerDevices()
|
||||||
except Exception:
|
except Exception:
|
||||||
errorLogging()
|
errorLogging()
|
||||||
finally:
|
|
||||||
pass
|
|
||||||
except Exception:
|
except Exception:
|
||||||
errorLogging()
|
errorLogging()
|
||||||
|
|
||||||
def startMonitoring(self):
|
def startMonitoring(self):
|
||||||
|
if self.monitoring_flag:
|
||||||
|
return
|
||||||
self.monitoring_flag = True
|
self.monitoring_flag = True
|
||||||
self.th_monitoring = Thread(target=self.monitoring)
|
self.th_monitoring = Thread(target=self.monitoring)
|
||||||
self.th_monitoring.daemon = True
|
self.th_monitoring.daemon = True
|
||||||
@@ -204,7 +285,12 @@ class DeviceManager:
|
|||||||
|
|
||||||
def stopMonitoring(self):
|
def stopMonitoring(self):
|
||||||
self.monitoring_flag = False
|
self.monitoring_flag = False
|
||||||
self.th_monitoring.join()
|
if getattr(self, "th_monitoring", None) is not None:
|
||||||
|
try:
|
||||||
|
self.th_monitoring.join(timeout=5)
|
||||||
|
except Exception:
|
||||||
|
# If join fails or thread is not joinable, ignore - it's a best-effort stop
|
||||||
|
pass
|
||||||
|
|
||||||
def setCallbackDefaultMicDevice(self, callback):
|
def setCallbackDefaultMicDevice(self, callback):
|
||||||
self.callback_default_mic_device = callback
|
self.callback_default_mic_device = callback
|
||||||
@@ -244,7 +330,10 @@ class DeviceManager:
|
|||||||
|
|
||||||
def runProcessBeforeUpdateMicDevices(self):
|
def runProcessBeforeUpdateMicDevices(self):
|
||||||
if isinstance(self.callback_process_before_update_mic_devices, Callable):
|
if isinstance(self.callback_process_before_update_mic_devices, Callable):
|
||||||
self.callback_process_before_update_mic_devices()
|
try:
|
||||||
|
self.callback_process_before_update_mic_devices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def setCallbackProcessAfterUpdateMicDevices(self, callback):
|
def setCallbackProcessAfterUpdateMicDevices(self, callback):
|
||||||
self.callback_process_after_update_mic_devices = callback
|
self.callback_process_after_update_mic_devices = callback
|
||||||
@@ -254,7 +343,10 @@ class DeviceManager:
|
|||||||
|
|
||||||
def runProcessAfterUpdateMicDevices(self):
|
def runProcessAfterUpdateMicDevices(self):
|
||||||
if isinstance(self.callback_process_after_update_mic_devices, Callable):
|
if isinstance(self.callback_process_after_update_mic_devices, Callable):
|
||||||
self.callback_process_after_update_mic_devices()
|
try:
|
||||||
|
self.callback_process_after_update_mic_devices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def setCallbackProcessBeforeUpdateSpeakerDevices(self, callback):
|
def setCallbackProcessBeforeUpdateSpeakerDevices(self, callback):
|
||||||
self.callback_process_before_update_speaker_devices = callback
|
self.callback_process_before_update_speaker_devices = callback
|
||||||
@@ -264,7 +356,10 @@ class DeviceManager:
|
|||||||
|
|
||||||
def runProcessBeforeUpdateSpeakerDevices(self):
|
def runProcessBeforeUpdateSpeakerDevices(self):
|
||||||
if isinstance(self.callback_process_before_update_speaker_devices, Callable):
|
if isinstance(self.callback_process_before_update_speaker_devices, Callable):
|
||||||
self.callback_process_before_update_speaker_devices()
|
try:
|
||||||
|
self.callback_process_before_update_speaker_devices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def setCallbackProcessAfterUpdateSpeakerDevices(self, callback):
|
def setCallbackProcessAfterUpdateSpeakerDevices(self, callback):
|
||||||
self.callback_process_after_update_speaker_devices = callback
|
self.callback_process_after_update_speaker_devices = callback
|
||||||
@@ -274,7 +369,10 @@ class DeviceManager:
|
|||||||
|
|
||||||
def runProcessAfterUpdateSpeakerDevices(self):
|
def runProcessAfterUpdateSpeakerDevices(self):
|
||||||
if isinstance(self.callback_process_after_update_speaker_devices, Callable):
|
if isinstance(self.callback_process_after_update_speaker_devices, Callable):
|
||||||
self.callback_process_after_update_speaker_devices()
|
try:
|
||||||
|
self.callback_process_after_update_speaker_devices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def noticeUpdateDevices(self):
|
def noticeUpdateDevices(self):
|
||||||
if self.update_flag_default_mic_device is True:
|
if self.update_flag_default_mic_device is True:
|
||||||
@@ -296,23 +394,38 @@ class DeviceManager:
|
|||||||
|
|
||||||
def setMicDefaultDevice(self):
|
def setMicDefaultDevice(self):
|
||||||
if isinstance(self.callback_default_mic_device, Callable):
|
if isinstance(self.callback_default_mic_device, Callable):
|
||||||
self.callback_default_mic_device(self.default_mic_device["host"]["name"], self.default_mic_device["device"]["name"])
|
try:
|
||||||
|
self.callback_default_mic_device(self.default_mic_device["host"]["name"], self.default_mic_device["device"]["name"])
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def setSpeakerDefaultDevice(self):
|
def setSpeakerDefaultDevice(self):
|
||||||
if isinstance(self.callback_default_speaker_device, Callable):
|
if isinstance(self.callback_default_speaker_device, Callable):
|
||||||
self.callback_default_speaker_device(self.default_speaker_device["device"]["name"])
|
try:
|
||||||
|
self.callback_default_speaker_device(self.default_speaker_device["device"]["name"])
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def setMicHostList(self):
|
def setMicHostList(self):
|
||||||
if isinstance(self.callback_host_list, Callable):
|
if isinstance(self.callback_host_list, Callable):
|
||||||
self.callback_host_list()
|
try:
|
||||||
|
self.callback_host_list()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def setMicDeviceList(self):
|
def setMicDeviceList(self):
|
||||||
if isinstance(self.callback_mic_device_list, Callable):
|
if isinstance(self.callback_mic_device_list, Callable):
|
||||||
self.callback_mic_device_list()
|
try:
|
||||||
|
self.callback_mic_device_list()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def setSpeakerDeviceList(self):
|
def setSpeakerDeviceList(self):
|
||||||
if isinstance(self.callback_speaker_device_list, Callable):
|
if isinstance(self.callback_speaker_device_list, Callable):
|
||||||
self.callback_speaker_device_list()
|
try:
|
||||||
|
self.callback_speaker_device_list()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
def getMicDevices(self):
|
def getMicDevices(self):
|
||||||
return self.mic_devices
|
return self.mic_devices
|
||||||
@@ -337,13 +450,15 @@ class DeviceManager:
|
|||||||
self.setSpeakerDeviceList()
|
self.setSpeakerDeviceList()
|
||||||
self.setSpeakerDefaultDevice()
|
self.setSpeakerDefaultDevice()
|
||||||
|
|
||||||
|
# Provide a module-level singleton. Call `device_manager.init()` explicitly to
|
||||||
|
# initialize audio resources and `device_manager.startMonitoring()` to begin
|
||||||
|
# background monitoring. This avoids side-effects during simple imports.
|
||||||
device_manager = DeviceManager()
|
device_manager = DeviceManager()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# print("getMicDevices()", device_manager.getMicDevices())
|
print("DeviceManager demo. Call device_manager.init() and device_manager.startMonitoring() to run live monitoring.")
|
||||||
# print("getDefaultMicDevice()", device_manager.getDefaultMicDevice())
|
try:
|
||||||
# print("getSpeakerDevices()", device_manager.getSpeakerDevices())
|
while True:
|
||||||
# print("getDefaultSpeakerDevice()", device_manager.getDefaultSpeakerDevice())
|
sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
while True:
|
print("exiting")
|
||||||
sleep(1)
|
|
||||||
93
src-python/docs/modules/device_manager_ref.md
Normal file
93
src-python/docs/modules/device_manager_ref.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# device_manager.py — デバイス検出と監視 (改訂版)
|
||||||
|
|
||||||
|
### 概要
|
||||||
|
`device_manager.py` はローカルのマイク(入力)とスピーカー(ループバックから抽出)を列挙し、デフォルトデバイスの変更やデバイスリストの変化を監視してコールバックで通知するユーティリティです。
|
||||||
|
|
||||||
|
設計上のポイント:
|
||||||
|
- Windows 固有の依存 (`comtypes`, `pyaudiowpatch` (PyAudio + WASAPI), `pycaw`) はオプショナルです。モジュールを import してもこれらが無ければ例外にならず、プレースホルダ値を返すようになっています。
|
||||||
|
- モジュールの import 時点では監視は開始されません。リソースやスレッドの副作用を避けるため、`init()` と `startMonitoring()` は呼び出し側で明示的に実行してください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 使い方(簡単な流れ)
|
||||||
|
|
||||||
|
1. モジュールをインポート
|
||||||
|
|
||||||
|
```py
|
||||||
|
from device_manager import device_manager
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 初期化(内部状態のセットアップ)
|
||||||
|
|
||||||
|
```py
|
||||||
|
device_manager.init()
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 監視の開始(バックグラウンドスレッド)
|
||||||
|
|
||||||
|
```py
|
||||||
|
device_manager.startMonitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 停止(アプリ終了時など)
|
||||||
|
|
||||||
|
```py
|
||||||
|
device_manager.stopMonitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 主な API
|
||||||
|
|
||||||
|
- `device_manager.init()`
|
||||||
|
- internal state の初期化。import 後に必ず呼ぶ必要はないが、実機デバイスを取得する前に呼ぶことを推奨します。
|
||||||
|
- `device_manager.startMonitoring()` / `device_manager.stopMonitoring()`
|
||||||
|
- 監視の開始 / 停止。`startMonitoring()` はデーモンスレッドを作成します。`stopMonitoring()` は best-effort で join を試みます。
|
||||||
|
- `device_manager.getMicDevices()`
|
||||||
|
- ホストごとにグループ化された入力デバイスの辞書を返します。例: `{ 'Realtek': [ {index: 2, name: 'Microphone (Realtek)'} ] }`。
|
||||||
|
- `device_manager.getDefaultMicDevice()` / `device_manager.getSpeakerDevices()` / `device_manager.getDefaultSpeakerDevice()`
|
||||||
|
- デフォルトデバイスやスピーカーループバックの情報を返します。
|
||||||
|
- `device_manager.forceUpdateAndSetMicDevices()` / `device_manager.forceUpdateAndSetSpeakerDevices()`
|
||||||
|
- 即時に update() を実行して対応するコールバックを呼びます。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### コールバック登録(例)
|
||||||
|
|
||||||
|
コールバックは例外を内部で捕捉してログを出すため、コールバック実装側でもエラーハンドリングしてください。
|
||||||
|
|
||||||
|
- `setCallbackDefaultMicDevice(callback)` — デフォルト入力が変わったときに `callback(host_name, device_name)` が呼ばれます。
|
||||||
|
- `setCallbackDefaultSpeakerDevice(callback)` — デフォルト出力が変わったときに `callback(device_name)` が呼ばれます。
|
||||||
|
- `setCallbackHostList(callback)` / `setCallbackMicDeviceList(callback)` / `setCallbackSpeakerDeviceList(callback)` — それぞれ list 変更時に `callback()` が呼ばれます。
|
||||||
|
- `setCallbackProcessBeforeUpdateMicDevices(callback)` / `setCallbackProcessAfterUpdateMicDevices(callback)` — 更新の前後に呼ばれるフックです。
|
||||||
|
|
||||||
|
簡単な例:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from device_manager import device_manager
|
||||||
|
|
||||||
|
def on_default_mic(host, device):
|
||||||
|
print('default mic changed', host, device)
|
||||||
|
|
||||||
|
device_manager.init()
|
||||||
|
device_manager.setCallbackDefaultMicDevice(on_default_mic)
|
||||||
|
device_manager.startMonitoring()
|
||||||
|
|
||||||
|
# 後で停止
|
||||||
|
# device_manager.stopMonitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 注意点 / トラブルシュート
|
||||||
|
|
||||||
|
- Windows 固有の依存が無い場合、`getMicDevices()` などはデフォルトのプレースホルダ(`NoHost` / `NoDevice`)を返します。実機のデバイス検出や WASAPI によるループバック検出は Windows 環境でのみ保証されます。
|
||||||
|
- `startMonitoring()` は監視用のデーモンスレッドを作るため、アプリケーションの終了時には `stopMonitoring()` を呼ぶかプロセスを終了してください。`stopMonitoring()` は join を行いますが、失敗した場合でも致命的にならないよう best-effort 実装です。
|
||||||
|
- コールバック内部で例外が発生してもモジュール側で捕捉してログ出力します(`utils.errorLogging()`)。コールバック側で詳細なハンドリングやリトライが必要な場合は呼び出し側で行ってください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 実装メモ
|
||||||
|
|
||||||
|
- `monitoring()` は可能なら Windows の COM (pycaw / MMNotificationClient) を使ってイベント駆動で待ち受け、失敗時や非Windows 環境では PyAudio を使ったポーリング(定期的な update()) にフォールバックします。
|
||||||
|
- 外部ライブラリが原因の例外は内部で捕捉し、`errorLogging()` を呼んで記録する設計です。
|
||||||
Reference in New Issue
Block a user