Refactoring how we are handling the silence.
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user