diff --git a/pyproject.toml b/pyproject.toml index 61e3c1a87..35b4c5664 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence" ] dependencies = [ + "aiofiles>=24.1.0,<25", "aiohttp>=3.11.12,<4", "audioop-lts~=0.2.1; python_version>='3.13'", "docstring_parser~=0.16", @@ -137,6 +138,20 @@ where = ["src"] [tool.setuptools.package-data] "pipecat" = ["py.typed"] +"pipecat.audio.dtmf" = [ + "src/pipecat/audio/dtmf/dtmf-0.wav", + "src/pipecat/audio/dtmf/dtmf-1.wav", + "src/pipecat/audio/dtmf/dtmf-2.wav", + "src/pipecat/audio/dtmf/dtmf-3.wav", + "src/pipecat/audio/dtmf/dtmf-4.wav", + "src/pipecat/audio/dtmf/dtmf-5.wav", + "src/pipecat/audio/dtmf/dtmf-6.wav", + "src/pipecat/audio/dtmf/dtmf-7.wav", + "src/pipecat/audio/dtmf/dtmf-8.wav", + "src/pipecat/audio/dtmf/dtmf-9.wav", + "src/pipecat/audio/dtmf/dtmf-pound.wav", + "src/pipecat/audio/dtmf/dtmf-star.wav", +] "pipecat.services.aws_nova_sonic" = ["src/pipecat/services/aws_nova_sonic/ready.wav"] [tool.pytest.ini_options] diff --git a/src/pipecat/audio/dtmf/dtmf-0.wav b/src/pipecat/audio/dtmf/dtmf-0.wav new file mode 100644 index 000000000..553eb4974 Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-0.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-1.wav b/src/pipecat/audio/dtmf/dtmf-1.wav new file mode 100644 index 000000000..ac852c451 Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-1.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-2.wav b/src/pipecat/audio/dtmf/dtmf-2.wav new file mode 100644 index 000000000..f4c5ca74c Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-2.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-3.wav b/src/pipecat/audio/dtmf/dtmf-3.wav new file mode 100644 index 000000000..141a7bcf1 Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-3.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-4.wav b/src/pipecat/audio/dtmf/dtmf-4.wav new file mode 100644 index 000000000..59b0cb029 Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-4.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-5.wav b/src/pipecat/audio/dtmf/dtmf-5.wav new file mode 100644 index 000000000..dcee26dfc Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-5.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-6.wav b/src/pipecat/audio/dtmf/dtmf-6.wav new file mode 100644 index 000000000..27f42571b Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-6.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-7.wav b/src/pipecat/audio/dtmf/dtmf-7.wav new file mode 100644 index 000000000..39769f832 Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-7.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-8.wav b/src/pipecat/audio/dtmf/dtmf-8.wav new file mode 100644 index 000000000..e623d805c Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-8.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-9.wav b/src/pipecat/audio/dtmf/dtmf-9.wav new file mode 100644 index 000000000..43ceb9cb7 Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-9.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-pound.wav b/src/pipecat/audio/dtmf/dtmf-pound.wav new file mode 100644 index 000000000..2c3984326 Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-pound.wav differ diff --git a/src/pipecat/audio/dtmf/dtmf-star.wav b/src/pipecat/audio/dtmf/dtmf-star.wav new file mode 100644 index 000000000..fa9fdc63a Binary files /dev/null and b/src/pipecat/audio/dtmf/dtmf-star.wav differ diff --git a/src/pipecat/audio/dtmf/utils.py b/src/pipecat/audio/dtmf/utils.py new file mode 100644 index 000000000..21855b73c --- /dev/null +++ b/src/pipecat/audio/dtmf/utils.py @@ -0,0 +1,70 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""DTMF audio utilities. + +This module provides functionality to load DTMF (Dual-Tone Multi-Frequency) +audio files corresponding to phone keypad entries. Audio data is cached +in-memory after first load to improve performance on subsequent accesses. +""" + +import asyncio +import io +import wave +from importlib.resources import files +from typing import Dict, Optional + +import aiofiles + +from pipecat.audio.dtmf.types import KeypadEntry +from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler +from pipecat.audio.utils import create_file_resampler + +__DTMF_LOCK__ = asyncio.Lock() +__DTMF_AUDIO__: Dict[KeypadEntry, bytes] = {} +__DTMF_RESAMPLER__: Optional[BaseAudioResampler] = None + +__DTMF_FILE_NAME = { + KeypadEntry.POUND: "dtmf-pound.wav", + KeypadEntry.STAR: "dtmf-star.wav", +} + + +async def load_dtmf_audio(button: KeypadEntry, *, sample_rate: int = 8000) -> bytes: + """Load audio for DTMF tones associated with the given button. + + Args: + button (KeypadEntry): The button for which the DTMF audio is to be loaded. + sample_rate (int, optional): The sample rate for the audio. Defaults to 8000. + + Returns: + bytes: The audio data for the DTMF tone as bytes. + """ + global __DTMF_AUDIO__, __DTMF_RESAMPLER__ + + async with __DTMF_LOCK__: + if button in __DTMF_AUDIO__: + return __DTMF_AUDIO__[button] + + if not __DTMF_RESAMPLER__: + __DTMF_RESAMPLER__ = create_file_resampler() + + dtmf_file_name = __DTMF_FILE_NAME.get(button, f"dtmf-{button.value}.wav") + dtmf_file_path = files("pipecat.audio.dtmf").joinpath(dtmf_file_name) + + async with aiofiles.open(dtmf_file_path, "rb") as f: + data = await f.read() + + with io.BytesIO(data) as buffer: + with wave.open(buffer, "rb") as wf: + audio = wf.readframes(wf.getnframes()) + in_sample_rate = wf.getframerate() + resampled_audio = await __DTMF_RESAMPLER__.resample( + audio, in_sample_rate, sample_rate + ) + __DTMF_AUDIO__[button] = resampled_audio + + return __DTMF_AUDIO__[button] diff --git a/uv.lock b/uv.lock index 1cc52c997..c5a225107 100644 --- a/uv.lock +++ b/uv.lock @@ -4189,6 +4189,7 @@ wheels = [ name = "pipecat-ai" source = { editable = "." } dependencies = [ + { name = "aiofiles" }, { name = "aiohttp" }, { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "docstring-parser" }, @@ -4409,6 +4410,7 @@ docs = [ requires-dist = [ { name = "accelerate", marker = "extra == 'moondream'", specifier = "~=1.10.0" }, { name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.0.0" }, + { name = "aiofiles", specifier = ">=24.1.0,<25" }, { name = "aiohttp", specifier = ">=3.11.12,<4" }, { name = "aiortc", marker = "extra == 'webrtc'", specifier = "~=1.11.0" }, { name = "anthropic", marker = "extra == 'anthropic'", specifier = "~=0.49.0" },