audio: add DeviceMonitor for device list polling
This commit is contained in:
@@ -10,6 +10,8 @@ from __future__ import annotations
|
|||||||
import array
|
import array
|
||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
|
import threading
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -30,6 +32,60 @@ def _apply_gain(pcm: bytes, gain: float) -> bytes:
|
|||||||
return samples.tobytes()
|
return samples.tobytes()
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceMonitor:
|
||||||
|
"""Poll sounddevice for device list changes.
|
||||||
|
|
||||||
|
Runs a daemon thread that compares a string fingerprint of
|
||||||
|
``sounddevice.query_devices()`` against a cached snapshot every
|
||||||
|
*interval* seconds. Fires *callback* when the list changes.
|
||||||
|
|
||||||
|
Uses ``threading.Event`` for cancellation (same pattern as
|
||||||
|
``ReconnectManager``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, callback: Callable[[], None], interval: float = 2.0):
|
||||||
|
self._callback = callback
|
||||||
|
self._interval = interval
|
||||||
|
self._cancel = threading.Event()
|
||||||
|
self._snapshot = self._device_fingerprint()
|
||||||
|
self._thread: threading.Thread | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _device_fingerprint() -> str:
|
||||||
|
"""Return a string snapshot of the current device list."""
|
||||||
|
try:
|
||||||
|
import sounddevice as sd
|
||||||
|
|
||||||
|
return str(sd.query_devices())
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Start polling in a daemon thread."""
|
||||||
|
if self._thread is not None and self._thread.is_alive():
|
||||||
|
return
|
||||||
|
self._cancel.clear()
|
||||||
|
self._snapshot = self._device_fingerprint()
|
||||||
|
self._thread = threading.Thread(target=self._poll, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Signal the poll loop to exit."""
|
||||||
|
self._cancel.set()
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
def _poll(self) -> None:
|
||||||
|
while not self._cancel.wait(timeout=self._interval):
|
||||||
|
current = self._device_fingerprint()
|
||||||
|
if current != self._snapshot:
|
||||||
|
self._snapshot = current
|
||||||
|
log.info("audio device list changed")
|
||||||
|
try:
|
||||||
|
self._callback()
|
||||||
|
except Exception:
|
||||||
|
log.exception("device change callback failed")
|
||||||
|
|
||||||
|
|
||||||
class AudioPipeline:
|
class AudioPipeline:
|
||||||
"""Manages audio input/output streams and Opus codec."""
|
"""Manages audio input/output streams and Opus codec."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user