introduce audio filters

This commit is contained in:
Aleix Conchillo Flaqué
2024-10-17 16:42:21 -07:00
parent 126324ca1b
commit abc8ede3d7
7 changed files with 97 additions and 4 deletions

View File

@@ -9,9 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Introduce output transport audio mixers. Output transport audio mixers can be
used, for example, to add background sounds or any other audio mixing
functionality before the output audio is actually written to the transport.
- Introduce input transport audio filters (`BaseAudioFilter`). Audio filters can
be used to remove background noises before audio is sent to VAD.
- Introduce output transport audio mixers (`BaseAudioMixer`). Output transport
audio mixers can be used, for example, to add background sounds or any other
audio mixing functionality before the output audio is actually written to the
transport.
- Added `GatedOpenAILLMContextAggregator`. This aggregator keeps the last
received OpenAI LLM context frame and it doesn't let it through until the

View File

View File

@@ -0,0 +1,47 @@
#
# Copyright (c) 2024, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
from abc import ABC, abstractmethod
from pipecat.frames.frames import FilterControlFrame
class BaseAudioFilter(ABC):
"""This is a base class for input transport audio filters. If an audio
filter is provided to the input transport it will be used to process audio
before VAD and before pushing it downstream. There are control frames to
update filter settings or to enable or disable the filter at runtime.
"""
@abstractmethod
async def start(self, sample_rate: int):
"""This will be called from the input transport when the transport is
started. It can be used to initialize the filter. The input transport
sample rate is provided so the filter can adjust to that sample rate.
"""
pass
@abstractmethod
async def stop(self):
"""This will be called from the input transport when the transport is
stopping.
"""
pass
@abstractmethod
async def process_frame(self, frame: FilterControlFrame):
"""This will be called when the input transport receives a
FilterControlFrame.
"""
pass
@abstractmethod
async def filter(self, audio: bytes) -> bytes:
pass

View File

@@ -584,9 +584,30 @@ class VADParamsUpdateFrame(ControlFrame):
params: VADParams
@dataclass
class FilterControlFrame(ControlFrame):
"""Base control frame for other audio filter frames."""
pass
@dataclass
class FilterUpdateSettingsFrame(FilterControlFrame):
"""Control frame to update filter settings."""
settings: Mapping[str, Any]
@dataclass
class FilterEnableFrame(FilterControlFrame):
"""Control frame to enable or disable the filter at runtime."""
enable: bool
@dataclass
class MixerControlFrame(ControlFrame):
"""Base control frame for other mixer frames."""
"""Base control frame for other audio mixer frames."""
pass

View File

@@ -41,6 +41,9 @@ class BaseInputTransport(FrameProcessor):
self._audio_task = None
async def start(self, frame: StartFrame):
# Start audio filter.
if self._params.audio_in_filter:
await self._params.audio_in_filter.start(self._params.audio_in_sample_rate)
# Create audio input queue and task if needed.
if self._params.audio_in_enabled or self._params.vad_enabled:
self._audio_in_queue = asyncio.Queue()
@@ -52,6 +55,9 @@ class BaseInputTransport(FrameProcessor):
self._audio_task.cancel()
await self._audio_task
self._audio_task = None
# Stop audio filter.
if self._params.audio_in_filter:
await self._params.audio_in_filter.stop()
async def cancel(self, frame: CancelFrame):
# Cancel and wait for the audio input task to finish.
@@ -165,6 +171,17 @@ class BaseInputTransport(FrameProcessor):
audio_passthrough = True
# If an audio filter is available, run it before VAD.
if self._params.audio_in_filter:
audio = await self._params.audio_in_filter.filter(
frame.audio, frame.sample_rate, frame.num_channels
)
frame = InputAudioRawFrame(
audio=audio,
sample_rate=frame.sample_rate,
num_channels=frame.num_channels,
)
# Check VAD and push event if necessary. We just care about
# changes from QUIET to SPEAKING and vice versa.
if self._params.vad_enabled:

View File

@@ -75,6 +75,7 @@ class BaseOutputTransport(FrameProcessor):
self._bot_speaking = False
async def start(self, frame: StartFrame):
# Start audio mixer.
if self._params.audio_out_mixer:
await self._params.audio_out_mixer.start(self._params.audio_out_sample_rate)
self._create_output_tasks()
@@ -82,6 +83,7 @@ class BaseOutputTransport(FrameProcessor):
async def stop(self, frame: EndFrame):
await self._cancel_output_tasks()
# Stop audio mixer.
if self._params.audio_out_mixer:
await self._params.audio_out_mixer.stop()

View File

@@ -13,6 +13,7 @@ from typing import Optional
from pydantic import ConfigDict
from pydantic.main import BaseModel
from pipecat.audio.filters.base_audio_filter import BaseAudioFilter
from pipecat.audio.mixers.base_audio_mixer import BaseAudioMixer
from pipecat.audio.vad.vad_analyzer import VADAnalyzer
from pipecat.processors.frame_processor import FrameProcessor
@@ -39,6 +40,7 @@ class TransportParams(BaseModel):
audio_in_enabled: bool = False
audio_in_sample_rate: int = 16000
audio_in_channels: int = 1
audio_in_filter: Optional[BaseAudioFilter] = None
vad_enabled: bool = False
vad_audio_passthrough: bool = False
vad_analyzer: VADAnalyzer | None = None