Latency lowered.

This commit is contained in:
Pbopbo
2026-03-18 17:37:34 +01:00
parent a605195646
commit 1687a2b790

View File

@@ -140,7 +140,7 @@ class AlsaArecordAudioInput(audio_io.AudioInput):
class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
"""PyALSA audio input with callback thread and ring buffer."""
"""PyALSA audio input with callback thread and ring buffer - supports mono/stereo."""
def __init__(self, device, pcm_format: audio_io.PcmFormat):
super().__init__()
@@ -153,10 +153,22 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
self._ring_lock = threading.Lock()
self._running = False
self._callback_thread = None
self._max_buffer_bytes = int(self._pcm_format.sample_rate * 0.1 * 2)
self._actual_channels = None
self._periodsize = None
def _open(self) -> audio_io.PcmFormat:
# ========== LATENCY CONFIGURATION ==========
# Adjust these parameters to tune latency vs stability
ALSA_PERIODSIZE = 120 # Samples per ALSA read (240@48kHz = 5ms, 120 = 2.5ms, 96 = 2ms)
ALSA_PERIODS = 2 # Number of periods in ALSA buffer (lower = less latency, more risk of underrun)
# Ring buffer: keep only 3 periods max to minimize latency (safety margin only)
# ===========================================
requested_rate = int(self._pcm_format.sample_rate)
requested_channels = int(self._pcm_format.channels)
self._periodsize = ALSA_PERIODSIZE
# Max ring buffer = 3 periods worth of data (tight coupling, minimal latency)
self._max_buffer_bytes = ALSA_PERIODSIZE * 3 * 2 * requested_channels
self._pcm = alsaaudio.PCM(
type=alsaaudio.PCM_CAPTURE,
@@ -164,17 +176,25 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
device=self._device,
)
self._pcm.setchannels(1)
self._pcm.setchannels(requested_channels)
self._pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
actual_rate = self._pcm.setrate(requested_rate)
self._pcm.setperiodsize(240)
self._pcm.setperiodsize(ALSA_PERIODSIZE)
try:
self._pcm.setperiods(ALSA_PERIODS)
except AttributeError:
pass # Some pyalsaaudio versions don't have setperiods()
logging.info("PyALSA: device=%s requested=%d actual=%d periodsize=240 (5ms)",
self._device, requested_rate, actual_rate)
ring_buf_samples = self._max_buffer_bytes // (2 * requested_channels)
ring_buf_ms = (ring_buf_samples / actual_rate) * 1000
logging.info("PyALSA: device=%s rate=%d ch=%d periodsize=%d (%.1fms) periods=%d ring_buf=%d samples (%.1fms)",
self._device, actual_rate, requested_channels, ALSA_PERIODSIZE,
(ALSA_PERIODSIZE / actual_rate) * 1000, ALSA_PERIODS, ring_buf_samples, ring_buf_ms)
if actual_rate != requested_rate:
logging.warning("PyALSA: Sample rate mismatch! requested=%d actual=%d", requested_rate, actual_rate)
self._actual_channels = requested_channels
self._running = True
self._callback_thread = threading.Thread(target=self._capture_loop, daemon=True)
self._callback_thread.start()
@@ -183,25 +203,28 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
audio_io.PcmFormat.Endianness.LITTLE,
audio_io.PcmFormat.SampleType.INT16,
actual_rate,
1,
requested_channels,
)
def _capture_loop(self):
first_read = True
hw_channels = None
while self._running:
try:
length, data = self._pcm.read()
if length > 0:
if first_read:
expected_bytes = 240 * 2 # 240 frames * 2 bytes/sample for mono
logging.info("PyALSA first capture: length=%d bytes=%d expected=%d", length, len(data), expected_bytes)
expected_mono = self._periodsize * 2
expected_stereo = self._periodsize * 2 * 2
hw_channels = 2 if len(data) == expected_stereo else 1
logging.info("PyALSA first capture: bytes=%d detected_hw_channels=%d requested_channels=%d",
len(data), hw_channels, self._actual_channels)
first_read = False
# If we got stereo data (480 bytes instead of 240), downsample to mono
if len(data) == 960: # 240 frames * 2 channels * 2 bytes = stereo
logging.warning("PyALSA: Got stereo data, converting to mono")
# Convert stereo hardware to mono if needed
if hw_channels == 2 and self._actual_channels == 1:
pcm_stereo = np.frombuffer(data, dtype=np.int16)
pcm_mono = pcm_stereo[::2] # Take only left channel
pcm_mono = pcm_stereo[::2]
data = pcm_mono.tobytes()
with self._ring_lock:
@@ -229,10 +252,13 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
result += chunk[:needed]
self._ring_buffer.appendleft(chunk[needed:])
else:
break
if len(result) < bytes_needed:
result += b'\x00' * (bytes_needed - len(result))
# Ring buffer empty - release lock and wait a bit
pass
if len(result) < bytes_needed:
# Don't busy-wait - sleep briefly to let capture thread fill buffer
import time
time.sleep(0.0001) # 0.1ms
return result