Merge pull request #1196 from pipecat-ai/aleix/audio-buffer-processor-continuous-intermittent-stream

AudioBufferProcessor: handle continuous and intermittent user audio
This commit is contained in:
Aleix Conchillo Flaqué
2025-02-10 16:07:12 -08:00
committed by GitHub
4 changed files with 49 additions and 18 deletions

View File

@@ -50,6 +50,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed an issue that was causing `AudioBufferProcessor` to not record
synchronized audio.
- Fixed an `RTVI` issue that was causing `bot-tts-text` messages to be sent - Fixed an `RTVI` issue that was causing `bot-tts-text` messages to be sent
before being processed by the output transport. before being processed by the output transport.

View File

@@ -90,7 +90,7 @@ async def run_bot(websocket_client: WebSocket, stream_sid: str, testing: bool):
# NOTE: Watch out! This will save all the conversation in memory. You can # NOTE: Watch out! This will save all the conversation in memory. You can
# pass `buffer_size` to get periodic callbacks. # pass `buffer_size` to get periodic callbacks.
audiobuffer = AudioBufferProcessor() audiobuffer = AudioBufferProcessor(user_continuous_stream=not testing)
pipeline = Pipeline( pipeline = Pipeline(
[ [

View File

@@ -124,7 +124,7 @@ async def run_client(client_name: str, server_url: str, duration_secs: int):
# NOTE: Watch out! This will save all the conversation in memory. You can # NOTE: Watch out! This will save all the conversation in memory. You can
# pass `buffer_size` to get periodic callbacks. # pass `buffer_size` to get periodic callbacks.
audiobuffer = AudioBufferProcessor() audiobuffer = AudioBufferProcessor(user_continuous_stream=False)
pipeline = Pipeline( pipeline = Pipeline(
[ [

View File

@@ -32,6 +32,10 @@ class AudioBufferProcessor(FrameProcessor):
in the case of stereo the left channel will be used for the user's audio and in the case of stereo the left channel will be used for the user's audio and
the right channel for the bot. the right channel for the bot.
Most of the time, user audio will be a continuous stream but it's possible
that in some cases only the spoken audio is sent. To accomodate for those
cases make sure to set `user_continuous_stream` accordingly.
""" """
def __init__( def __init__(
@@ -40,6 +44,7 @@ class AudioBufferProcessor(FrameProcessor):
sample_rate: Optional[int] = None, sample_rate: Optional[int] = None,
num_channels: int = 1, num_channels: int = 1,
buffer_size: int = 0, buffer_size: int = 0,
user_continuous_stream: bool = True,
**kwargs, **kwargs,
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
@@ -47,10 +52,12 @@ class AudioBufferProcessor(FrameProcessor):
self._sample_rate = 0 self._sample_rate = 0
self._num_channels = num_channels self._num_channels = num_channels
self._buffer_size = buffer_size self._buffer_size = buffer_size
self._user_continuous_stream = user_continuous_stream
self._user_audio_buffer = bytearray() self._user_audio_buffer = bytearray()
self._bot_audio_buffer = bytearray() self._bot_audio_buffer = bytearray()
# Intermittent (non continous user stream variables)
self._last_user_frame_at = 0 self._last_user_frame_at = 0
self._last_bot_frame_at = 0 self._last_bot_frame_at = 0
@@ -98,7 +105,40 @@ class AudioBufferProcessor(FrameProcessor):
if isinstance(frame, StartFrame): if isinstance(frame, StartFrame):
self._update_sample_rate(frame) self._update_sample_rate(frame)
if self._recording and isinstance(frame, InputAudioRawFrame): if self._recording:
if self._user_continuous_stream:
await self._handle_continuous_stream(frame)
else:
await self._handle_intermittent_stream(frame)
if self._buffer_size > 0 and len(self._user_audio_buffer) > self._buffer_size:
await self._call_on_audio_data_handler()
if isinstance(frame, (CancelFrame, EndFrame)):
await self.stop_recording()
await self.push_frame(frame, direction)
def _update_sample_rate(self, frame: StartFrame):
self._sample_rate = self._init_sample_rate or frame.audio_out_sample_rate
async def _handle_continuous_stream(self, frame: Frame):
if isinstance(frame, InputAudioRawFrame):
# Add user audio.
resampled = await self._resample_audio(frame)
self._user_audio_buffer.extend(resampled)
# Sync the bot's buffer to the user's buffer by adding silence if needed
if len(self._user_audio_buffer) > len(self._bot_audio_buffer):
silence_size = len(self._user_audio_buffer) - len(self._bot_audio_buffer)
silence = b"\x00" * silence_size
self._bot_audio_buffer.extend(silence)
elif self._recording and isinstance(frame, OutputAudioRawFrame):
# Add bot audio.
resampled = await self._resample_audio(frame)
self._bot_audio_buffer.extend(resampled)
async def _handle_intermittent_stream(self, frame: Frame):
if isinstance(frame, InputAudioRawFrame):
# Add silence if we need to. # Add silence if we need to.
silence = self._compute_silence(self._last_user_frame_at) silence = self._compute_silence(self._last_user_frame_at)
self._user_audio_buffer.extend(silence) self._user_audio_buffer.extend(silence)
@@ -117,17 +157,6 @@ class AudioBufferProcessor(FrameProcessor):
# Save time of frame so we can compute silence. # Save time of frame so we can compute silence.
self._last_bot_frame_at = time.time() self._last_bot_frame_at = time.time()
if self._buffer_size > 0 and len(self._user_audio_buffer) > self._buffer_size:
await self._call_on_audio_data_handler()
if isinstance(frame, (CancelFrame, EndFrame)):
await self.stop_recording()
await self.push_frame(frame, direction)
def _update_sample_rate(self, frame: StartFrame):
self._sample_rate = self._init_sample_rate or frame.audio_out_sample_rate
async def _call_on_audio_data_handler(self): async def _call_on_audio_data_handler(self):
if not self.has_audio() or not self._recording: if not self.has_audio() or not self._recording:
return return
@@ -155,10 +184,9 @@ class AudioBufferProcessor(FrameProcessor):
def _compute_silence(self, from_time: float) -> bytes: def _compute_silence(self, from_time: float) -> bytes:
quiet_time = time.time() - from_time quiet_time = time.time() - from_time
# We should get audio frames very frequently. We pick 100ms because # We should get audio frames very frequently. We introduce silence only
# that's big enough, but it could be even a bit slower since we usually # if there's a big enough gap of 1s.
# do 20ms audio frames. if from_time == 0 or quiet_time < 1.0:
if from_time == 0 or quiet_time < 0.1:
return b"" return b""
num_bytes = int(quiet_time * self._sample_rate) * 2 num_bytes = int(quiet_time * self._sample_rate) * 2
silence = b"\x00" * num_bytes silence = b"\x00" * num_bytes