tune consumption of samples

This commit is contained in:
2025-11-14 15:53:55 +01:00
parent 204e287075
commit d932d07e7e
2 changed files with 30 additions and 49 deletions

View File

@@ -58,17 +58,8 @@ from auracast.utils.webrtc_audio_input import WebRTCAudioInput
# Patch sounddevice.InputStream globally to use low-latency settings # Patch sounddevice.InputStream globally to use low-latency settings
import sounddevice as sd import sounddevice as sd
from collections import deque from collections import deque
import statistics
def popleft_n(d: deque, n: int) -> bytes:
# pops up to n items (handles short deques)
# if the deque was too short, fill with 0s and print a warning
it = (d.popleft() for _ in range(min(n, len(d))))
if len(d) < n:
logging.warning("SoundDeviceAudioInput: deque was too short, requested %d, got %d", n, len(d))
return b"".join(it)
class ModSoundDeviceAudioInput(audio_io.SoundDeviceAudioInput): class ModSoundDeviceAudioInput(audio_io.SoundDeviceAudioInput):
"""Patched SoundDeviceAudioInput with low-latency capture and adaptive resampling.""" """Patched SoundDeviceAudioInput with low-latency capture and adaptive resampling."""
@@ -94,13 +85,10 @@ class ModSoundDeviceAudioInput(audio_io.SoundDeviceAudioInput):
# Target ~2 ms blocksize (48 kHz -> 96 frames). For other rates, keep ~2 ms. # Target ~2 ms blocksize (48 kHz -> 96 frames). For other rates, keep ~2 ms.
_sr = int(self._pcm_format.sample_rate) _sr = int(self._pcm_format.sample_rate)
self.log_counter0=0 self.counter=0
self._runavg_samps = deque(maxlen=30)
self._runavg = 0
self.max_avail=0 self.max_avail=0
self.logfile_name="available_samples.txt" self.logfile_name="available_samples.txt"
self.blocksize = 480 self.blocksize = 120
self._frames_offset = 0
if os.path.exists(self.logfile_name): if os.path.exists(self.logfile_name):
os.remove(self.logfile_name) os.remove(self.logfile_name)
@@ -111,7 +99,7 @@ class ModSoundDeviceAudioInput(audio_io.SoundDeviceAudioInput):
channels=self._pcm_format.channels, channels=self._pcm_format.channels,
dtype='int16', dtype='int16',
blocksize=self.blocksize, blocksize=self.blocksize,
latency=0.005, latency=0.004,
) )
self._stream.start() self._stream.start()
@@ -123,50 +111,43 @@ class ModSoundDeviceAudioInput(audio_io.SoundDeviceAudioInput):
) )
def _read(self, frame_size: int) -> bytes: def _read(self, frame_size: int) -> bytes:
"""Generate frames while minimally adjusting frames_offset to keep _runavg ~ blocksize/2.""" """Read PCM samples from the stream."""
# Persist frames_offset across calls; clamp within 1% of blocksize
max_offset = 30 #if self.counter % 50 == 0:
target = 0.5*self.blocksize frame_size = frame_size + 1 # consume samples a little faster to avoid latency akkumulation
deadband = 2
pcm_buffer, overflowed = self._stream.read(frame_size + self._frames_offset) pcm_buffer, overflowed = self._stream.read(frame_size)
if overflowed: if overflowed:
logging.warning("SoundDeviceAudioInput: overflowed") logging.warning("SoundDeviceAudioInput: overflowed")
available = self._stream.read_available n_available = self._stream.read_available
if available > 0:
self.max_avail = max(self.max_avail, available)
self._runavg_samps.append(available) # adapt = n_available > 20
self._runavg = max(self._runavg_samps) # statistics.median(self._runavg_samps) # if adapt:
# pcm_extra, overflowed = self._stream.read(3)
# logging.info('consuming extra samples, available was %d', n_available)
# if overflowed:
# logging.warning("SoundDeviceAudioInput: overflowed")
# out = bytes(pcm_buffer) + bytes(pcm_extra)
# else:
out = bytes(pcm_buffer)
# Minimal feedback: nudge frames_offset by 1 to target blocksize/2 with small deadband self.max_avail = max(self.max_avail, n_available)
increment = self._runavg > (target + deadband) or overflowed
decrement = self._runavg < (target - deadband)
clipped_upper = self._frames_offset >= max_offset
clipped_lower = self._frames_offset <= -1 * max_offset
self._frames_offset = 0
if increment and not clipped_upper:
self._frames_offset += 1
elif decrement and not clipped_lower:
self._frames_offset -= 1
#Diagnostics #Diagnostics
with open(self.logfile_name, "a", encoding="utf-8") as f: #with open(self.logfile_name, "a", encoding="utf-8") as f:
f.write(f"{available}, {round(self._runavg, 2)}, {self._frames_offset}, {overflowed}\n") # f.write(f"{n_available}, {adapt}, {round(self._runavg, 2)}, {overflowed}\n")
if self.log_counter0 % 500 == 0: if self.counter % 500 == 0:
logging.info( logging.info(
"read available=%d, max=%d, runavg=%.3f, frames_offset=%d (max %d)", "read available=%d, max=%d, latency:%d",
available, self.max_avail, round(self._runavg, 2), self._frames_offset, max_offset n_available, self.max_avail, self._stream.latency
) )
self.max_avail = 0 self.max_avail = 0
self.log_counter0 += 1 self.counter += 1
return bytes(pcm_buffer) return out
audio_io.SoundDeviceAudioInput = ModSoundDeviceAudioInput audio_io.SoundDeviceAudioInput = ModSoundDeviceAudioInput

View File

@@ -6,8 +6,8 @@ pcm.ch1 {
channels 2 channels 2
rate 48000 rate 48000
format S16_LE format S16_LE
period_size 480 # 2.5ms period_size 120
buffer_size 960 buffer_size 240
} }
bindings.0 0 bindings.0 0
} }
@@ -21,8 +21,8 @@ pcm.ch2 {
channels 2 channels 2
rate 48000 rate 48000
format S16_LE format S16_LE
period_size 480 period_size 120
buffer_size 960 buffer_size 240
} }
bindings.0 1 bindings.0 1
} }