TTSService: resample _stream_audio_frames_from_iterator() input audio if needed

This commit is contained in:
Aleix Conchillo Flaqué
2026-01-28 23:54:17 -08:00
parent 875614ff7a
commit 11daa43b1b

View File

@@ -24,6 +24,7 @@ from typing import (
from loguru import logger
from pipecat.audio.utils import create_stream_resampler
from pipecat.frames.frames import (
AggregatedTextFrame,
AggregationType,
@@ -202,6 +203,8 @@ class TTSService(AIService):
)
self._text_filters = [text_filter]
self._resampler = create_stream_resampler()
self._stop_frame_task: Optional[asyncio.Task] = None
self._stop_frame_queue: asyncio.Queue = asyncio.Queue()
@@ -505,12 +508,40 @@ class TTSService(AIService):
await self._stop_frame_queue.put(frame)
async def _stream_audio_frames_from_iterator(
self, iterator: AsyncIterator[bytes], *, strip_wav_header: bool
self,
iterator: AsyncIterator[bytes],
*,
strip_wav_header: bool = False,
in_sample_rate: Optional[int] = None,
) -> AsyncGenerator[Frame, None]:
"""Stream audio frames from an async byte iterator with optional resampling.
For WAV data, use `strip_wav_header=True` to strip the header and
auto-detect the source sample rate. For raw PCM data, pass
`in_sample_rate` directly. Audio is resampled to `self.sample_rate` when
the source rate differs.
Args:
iterator: Async iterator yielding audio bytes.
strip_wav_header: Strip WAV header and parse source sample rate from it.
in_sample_rate: Source sample rate for raw PCM data. Overrides
WAV-detected rate if both are provided.
"""
buffer = bytearray()
source_sample_rate = in_sample_rate
need_to_strip_wav_header = strip_wav_header
async def maybe_resample(audio: bytes) -> bytes:
if source_sample_rate and source_sample_rate != self.sample_rate:
return await self._resampler.resample(audio, source_sample_rate, self.sample_rate)
return audio
async for chunk in iterator:
if need_to_strip_wav_header and chunk.startswith(b"RIFF"):
# Parse sample rate from WAV header (bytes 24-28, little-endian uint32).
if len(chunk) >= 44 and source_sample_rate is None:
source_sample_rate = int.from_bytes(chunk[24:28], "little")
chunk = chunk[44:]
need_to_strip_wav_header = False
@@ -520,19 +551,18 @@ class TTSService(AIService):
# Round to nearest even number.
aligned_length = len(buffer) & ~1 # 111111111...11110
if aligned_length > 0:
aligned_chunk = buffer[:aligned_length]
aligned_chunk = await maybe_resample(bytes(buffer[:aligned_length]))
buffer = buffer[aligned_length:] # keep any leftover byte
if len(aligned_chunk) > 0:
frame = TTSAudioRawFrame(bytes(aligned_chunk), self.sample_rate, 1)
yield frame
yield TTSAudioRawFrame(aligned_chunk, self.sample_rate, 1)
if len(buffer) > 0:
# Make sure we don't need an extra padding byte.
if len(buffer) % 2 == 1:
buffer.extend(b"\x00")
frame = TTSAudioRawFrame(bytes(buffer), self.sample_rate, 1)
yield frame
audio = await maybe_resample(bytes(buffer))
yield TTSAudioRawFrame(audio, self.sample_rate, 1)
async def _handle_interruption(self, frame: InterruptionFrame, direction: FrameDirection):
self._processing_text = False