ASRC: Adds NONBLOCK read from ALSA buffer; controls the amount of frames in the ALSA buffer; Adds resampling to get rid of audio glitches; no latency buildup anymore.
This commit is contained in:
@@ -30,6 +30,7 @@ import time
|
||||
import threading
|
||||
|
||||
import numpy as np # for audio down-mix
|
||||
import samplerate
|
||||
import os
|
||||
|
||||
import lc3 # type: ignore # pylint: disable=E0401
|
||||
@@ -153,10 +154,13 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
|
||||
self._periodsize = None
|
||||
self._hw_channels = None
|
||||
self._first_read = True
|
||||
self._resampler = None
|
||||
self._resampler_buffer = np.empty(0, dtype=np.float32)
|
||||
|
||||
def _open(self) -> audio_io.PcmFormat:
|
||||
ALSA_PERIODSIZE = 240
|
||||
ALSA_PERIODS = 4
|
||||
ALSA_MODE = alsaaudio.PCM_NONBLOCK
|
||||
|
||||
requested_rate = int(self._pcm_format.sample_rate)
|
||||
requested_channels = int(self._pcm_format.channels)
|
||||
@@ -164,7 +168,7 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
|
||||
|
||||
self._pcm = alsaaudio.PCM(
|
||||
type=alsaaudio.PCM_CAPTURE,
|
||||
mode=alsaaudio.PCM_NORMAL,
|
||||
mode=ALSA_MODE,
|
||||
device=self._device,
|
||||
periods=ALSA_PERIODS,
|
||||
)
|
||||
@@ -174,14 +178,16 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
|
||||
actual_rate = self._pcm.setrate(requested_rate)
|
||||
self._pcm.setperiodsize(ALSA_PERIODSIZE)
|
||||
|
||||
logging.info("PyALSA: device=%s rate=%d ch=%d periodsize=%d (%.1fms) periods=%d mode=NONBLOCK",
|
||||
logging.info("PyALSA: device=%s rate=%d ch=%d periodsize=%d (%.1fms) periods=%d mode=%s",
|
||||
self._device, actual_rate, requested_channels, ALSA_PERIODSIZE,
|
||||
(ALSA_PERIODSIZE / actual_rate) * 1000, ALSA_PERIODS)
|
||||
(ALSA_PERIODSIZE / actual_rate) * 1000, ALSA_PERIODS, ALSA_MODE)
|
||||
|
||||
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._resampler = samplerate.Resampler('sinc_fastest', channels=requested_channels)
|
||||
self._resampler_buffer = np.empty(0, dtype=np.float32)
|
||||
|
||||
self._bang_bang = 0
|
||||
|
||||
@@ -193,14 +199,22 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
|
||||
)
|
||||
|
||||
def _read(self, frame_size: int) -> bytes:
|
||||
bytes_needed = frame_size * 2 * self._actual_channels
|
||||
result = b''
|
||||
|
||||
try:
|
||||
avail = self._pcm.avail()
|
||||
logging.debug("PyALSA: avail before read: %d", avail)
|
||||
length, data = self._pcm.read_sw(frame_size + self._bang_bang)
|
||||
avail = self._pcm.avail()
|
||||
self._bang_bang = 1 if avail > 50 else 0
|
||||
logging.info("PyALSA: read length=%d, data length=%d, avail=%d, bang_bang=%d", length, len(data), avail, self._bang_bang)
|
||||
SETPOINT = 120
|
||||
TOLERANCE = 40
|
||||
if avail < SETPOINT - TOLERANCE:
|
||||
self._bang_bang = -1
|
||||
elif avail > SETPOINT + TOLERANCE:
|
||||
self._bang_bang = 1
|
||||
else:
|
||||
self._bang_bang = 0
|
||||
|
||||
|
||||
logging.debug("PyALSA: read length=%d, data length=%d, avail=%d, bang_bang=%d", length, len(data), avail, self._bang_bang)
|
||||
|
||||
if length > 0:
|
||||
if self._first_read:
|
||||
@@ -217,51 +231,39 @@ class PyAlsaAudioInput(audio_io.ThreadedAudioInput):
|
||||
pcm_mono = pcm_stereo[::2]
|
||||
data = pcm_mono.tobytes()
|
||||
|
||||
result += data
|
||||
actual_samples = len(data) // (2 * self._actual_channels)
|
||||
ratio = frame_size / actual_samples
|
||||
pcm_f32 = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
|
||||
if self._actual_channels > 1:
|
||||
pcm_f32 = pcm_f32.reshape(-1, self._actual_channels)
|
||||
resampled = self._resampler.process(pcm_f32, ratio, end_of_input=False)
|
||||
if self._actual_channels > 1:
|
||||
resampled = resampled.reshape(-1)
|
||||
self._resampler_buffer = np.concatenate([self._resampler_buffer, resampled])
|
||||
else:
|
||||
logging.info("PyALSA: No data read from ALSA")
|
||||
logging.warning("PyALSA: No data read from ALSA")
|
||||
self._resampler_buffer = np.concatenate([
|
||||
self._resampler_buffer,
|
||||
np.zeros(frame_size * self._actual_channels, dtype=np.float32),
|
||||
])
|
||||
except alsaaudio.ALSAAudioError as e:
|
||||
logging.info("PyALSA: ALSA read error: %s", e)
|
||||
logging.error("PyALSA: ALSA read error: %s", e)
|
||||
self._resampler_buffer = np.concatenate([
|
||||
self._resampler_buffer,
|
||||
np.zeros(frame_size * self._actual_channels, dtype=np.float32),
|
||||
])
|
||||
|
||||
|
||||
logging.info("PyALSA: result length=%d, frame_size=%d", len(result), frame_size)
|
||||
needed = frame_size * self._actual_channels
|
||||
if len(self._resampler_buffer) < needed:
|
||||
pad = np.zeros(needed - len(self._resampler_buffer), dtype=np.float32)
|
||||
self._resampler_buffer = np.concatenate([self._resampler_buffer, pad])
|
||||
logging.debug("PyALSA: padded buffer with %d samples", needed - len(self._resampler_buffer))
|
||||
|
||||
if len(result) < bytes_needed:
|
||||
result += b'\x00' * (bytes_needed - len(result))
|
||||
|
||||
return result[:bytes_needed]
|
||||
output = self._resampler_buffer[:needed]
|
||||
self._resampler_buffer = self._resampler_buffer[needed:]
|
||||
|
||||
def _read2(self, frame_size: int) -> bytes:
|
||||
bytes_needed = frame_size * 2 * self._actual_channels
|
||||
result = b''
|
||||
|
||||
while len(result) < bytes_needed:
|
||||
try:
|
||||
length, data = self._pcm.read()
|
||||
if length > 0:
|
||||
if self._first_read:
|
||||
expected_mono = self._periodsize * 2
|
||||
expected_stereo = self._periodsize * 2 * 2
|
||||
self._hw_channels = 2 if len(data) == expected_stereo else 1
|
||||
logging.info("PyALSA first read: bytes=%d detected_hw_channels=%d requested_channels=%d",
|
||||
len(data), self._hw_channels, self._actual_channels)
|
||||
self._first_read = False
|
||||
|
||||
if self._hw_channels == 2 and self._actual_channels == 1:
|
||||
pcm_stereo = np.frombuffer(data, dtype=np.int16)
|
||||
pcm_mono = pcm_stereo[::2]
|
||||
data = pcm_mono.tobytes()
|
||||
|
||||
result += data
|
||||
else:
|
||||
break
|
||||
except alsaaudio.ALSAAudioError:
|
||||
break
|
||||
|
||||
if len(result) < bytes_needed:
|
||||
result += b'\x00' * (bytes_needed - len(result))
|
||||
|
||||
return result[:bytes_needed]
|
||||
logging.debug("PyALSA: resampler_buffer remaining=%d", len(self._resampler_buffer))
|
||||
return np.clip(output * 32767.0, -32768, 32767).astype(np.int16).tobytes()
|
||||
|
||||
|
||||
|
||||
|
||||
0
src/service/update_and_run_server_and_frontend.sh
Normal file → Executable file
0
src/service/update_and_run_server_and_frontend.sh
Normal file → Executable file
Reference in New Issue
Block a user