TTSService: resample _stream_audio_frames_from_iterator() input audio if needed
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user