Refactoring how we are handling the silence.

This commit is contained in:
filipi87
2026-05-20 17:41:11 -03:00
parent e7bad7a007
commit 6a238e0d62

View File

@@ -23,6 +23,8 @@ load_dotenv(override=True)
TRUE_SAMPLE_RATE = 24000 TRUE_SAMPLE_RATE = 24000
DECLARED_SAMPLE_RATE = 48000 DECLARED_SAMPLE_RATE = 48000
SPEEDUP = DECLARED_SAMPLE_RATE // TRUE_SAMPLE_RATE SPEEDUP = DECLARED_SAMPLE_RATE // TRUE_SAMPLE_RATE
CHUNK_BYTES = int(TRUE_SAMPLE_RATE * 20 / 1000) * 2 # 20 ms, 16-bit mono
MIN_AUDIO_BUFFER = CHUNK_BYTES * 5 # 100 ms pre-buffer
def completion_callback(future): def completion_callback(future):
@@ -52,9 +54,6 @@ class DailyProxyApp(EventHandler):
# Raw PCM buffer — filled at DECLARED_SAMPLE_RATE speed, drained at TRUE_SAMPLE_RATE speed. # Raw PCM buffer — filled at DECLARED_SAMPLE_RATE speed, drained at TRUE_SAMPLE_RATE speed.
self._buffer = bytearray() self._buffer = bytearray()
self._audio_task: asyncio.Task | None = None self._audio_task: asyncio.Task | None = None
# Tracks whether the previous frame was silence, to detect speech→silence transitions.
# Initialised True so leading silence before first speech is dropped.
self._last_was_silence: bool = True
self._client: CallClient = CallClient(event_handler=self) self._client: CallClient = CallClient(event_handler=self)
self._client.update_subscription_profiles( self._client.update_subscription_profiles(
@@ -176,13 +175,12 @@ class DailyProxyApp(EventHandler):
""" """
new_bytes = audio_data.audio_frames new_bytes = audio_data.audio_frames
if self._is_silence(new_bytes): if self._is_silence(new_bytes):
if not self._last_was_silence: if len(self._buffer) < MIN_AUDIO_BUFFER:
# First silence after speech: mark the pause with one chunk. # Below pre-buffer threshold: add silence so the buffer fills up.
self._buffer.extend(bytes(len(new_bytes) // SPEEDUP)) self._buffer.extend(new_bytes)
self._last_was_silence = True # else: buffer is healthy, discard silence so it can drain.
return return
self._last_was_silence = False
self._buffer.extend(new_bytes) self._buffer.extend(new_bytes)
def _audio_data_received(self, participant_id: str, audio_data: AudioData, audio_source: str): def _audio_data_received(self, participant_id: str, audio_data: AudioData, audio_source: str):
@@ -192,7 +190,6 @@ class DailyProxyApp(EventHandler):
"""Clear the audio buffer, mimicking the avatar stopping mid-speech.""" """Clear the audio buffer, mimicking the avatar stopping mid-speech."""
dropped = len(self._buffer) dropped = len(self._buffer)
self._buffer.clear() self._buffer.clear()
self._last_was_silence = True
logger.info( logger.info(
f"Interrupt received — dropped {dropped}B ({dropped / (TRUE_SAMPLE_RATE * 2):.3f}s) from buffer" f"Interrupt received — dropped {dropped}B ({dropped / (TRUE_SAMPLE_RATE * 2):.3f}s) from buffer"
) )
@@ -215,25 +212,21 @@ class DailyProxyApp(EventHandler):
dry it re-enters the waiting state so the next burst also gets the dry it re-enters the waiting state so the next burst also gets the
pre-buffer delay. pre-buffer delay.
""" """
chunk_bytes = int(TRUE_SAMPLE_RATE * 20 / 1000) * 2 # 20 ms, 16-bit mono
min_audio_buffer = chunk_bytes * 5 # 100 ms pre-buffer
buffering = True buffering = True
last_log_time = self._loop.time() last_log_time = self._loop.time()
while True: while True:
if buffering: if buffering:
if len(self._buffer) >= min_audio_buffer: if len(self._buffer) >= MIN_AUDIO_BUFFER:
buffering = False buffering = False
logger.debug( logger.debug(f"Pre-buffer reached ({MIN_AUDIO_BUFFER}B) — starting playback")
f"Pre-buffer reached ({min_audio_buffer}B) — starting playback"
)
else: else:
await asyncio.sleep(0.001) await asyncio.sleep(0.001)
continue continue
if len(self._buffer) >= chunk_bytes: if len(self._buffer) >= CHUNK_BYTES:
chunk = bytes(self._buffer[:chunk_bytes]) chunk = bytes(self._buffer[:CHUNK_BYTES])
del self._buffer[:chunk_bytes] del self._buffer[:CHUNK_BYTES]
future = asyncio.get_running_loop().create_future() future = asyncio.get_running_loop().create_future()
self._audio_source.write_frames(chunk, completion=completion_callback(future)) self._audio_source.write_frames(chunk, completion=completion_callback(future))