feat: refactor audio input to use dedicated reader thread instead of per-frame executor
- Replaced per-frame `run_in_executor` calls with single background reader thread in `ThreadedAudioInput` - Reader thread continuously calls `_read()` and enqueues data via `call_soon_threadsafe` to asyncio.Queue - Reduces per-frame scheduling overhead and context-switch jitter while preserving async API - Added thread lifecycle management: lazy start on first `frames()` call, graceful stop in `aclose()` - Update
This commit is contained in:
44
audio/io.py
44
audio/io.py
@@ -27,6 +27,7 @@ import sys
|
||||
import wave
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import TYPE_CHECKING, AsyncGenerator, BinaryIO
|
||||
import threading
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -406,6 +407,9 @@ class ThreadedAudioInput(AudioInput):
|
||||
def __init__(self) -> None:
|
||||
self._thread_pool = ThreadPoolExecutor(1)
|
||||
self._pcm_samples: asyncio.Queue[bytes] = asyncio.Queue()
|
||||
self._reader_thread: threading.Thread | None = None
|
||||
self._running: bool = False
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
@abc.abstractmethod
|
||||
def _read(self, frame_size: int) -> bytes:
|
||||
@@ -424,12 +428,46 @@ class ThreadedAudioInput(AudioInput):
|
||||
)
|
||||
|
||||
async def frames(self, frame_size: int) -> AsyncGenerator[bytes]:
|
||||
while pcm_sample := await asyncio.get_running_loop().run_in_executor(
|
||||
self._thread_pool, self._read, frame_size
|
||||
):
|
||||
# Start a dedicated reader thread on first use to avoid per-frame
|
||||
# run_in_executor overhead while preserving the same async API.
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._loop = asyncio.get_running_loop()
|
||||
|
||||
def _reader() -> None:
|
||||
try:
|
||||
while self._running:
|
||||
pcm_sample = self._read(frame_size)
|
||||
if not pcm_sample:
|
||||
# Propagate termination to the async generator.
|
||||
if self._loop is not None:
|
||||
self._loop.call_soon_threadsafe(
|
||||
self._pcm_samples.put_nowait, b""
|
||||
)
|
||||
break
|
||||
if self._loop is not None:
|
||||
self._loop.call_soon_threadsafe(
|
||||
self._pcm_samples.put_nowait, pcm_sample
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("ThreadedAudioInput reader thread failed")
|
||||
|
||||
self._reader_thread = threading.Thread(target=_reader, daemon=True)
|
||||
self._reader_thread.start()
|
||||
|
||||
while True:
|
||||
pcm_sample = await self._pcm_samples.get()
|
||||
if not pcm_sample:
|
||||
break
|
||||
yield pcm_sample
|
||||
|
||||
async def aclose(self) -> None:
|
||||
# Stop reader thread first so no more _read() calls are issued.
|
||||
self._running = False
|
||||
if self._reader_thread is not None:
|
||||
self._reader_thread.join(timeout=1.0)
|
||||
self._reader_thread = None
|
||||
|
||||
await asyncio.get_running_loop().run_in_executor(self._thread_pool, self._close)
|
||||
self._thread_pool.shutdown()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user