audio: add DeviceMonitor for device list polling
This commit is contained in:
@@ -10,6 +10,8 @@ from __future__ import annotations
|
||||
import array
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
from typing import Callable
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,6 +32,60 @@ def _apply_gain(pcm: bytes, gain: float) -> bytes:
|
||||
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:
|
||||
"""Manages audio input/output streams and Opus codec."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user