Skip to content

Commit 1a89a9a

Browse files
committed
refactor: build WAV containers with pcm_to_wav
The websocket transports, segmented STT, audio context messages, and the eval recorder each hand-rolled the same wave boilerplate.
1 parent 08002bc commit 1a89a9a

7 files changed

Lines changed: 28 additions & 77 deletions

File tree

src/pipecat/evals/speech.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
rate.
2828
"""
2929

30-
import asyncio
3130
import hashlib
3231
import importlib
3332
import os
@@ -38,6 +37,7 @@
3837

3938
from loguru import logger
4039

40+
from pipecat.audio.utils import pcm_to_wav
4141
from pipecat.evals.services import cartesia_service, kokoro_service
4242
from pipecat.services.tts_service import TTSService
4343
from pipecat.services.websocket_service import WebsocketService
@@ -350,8 +350,4 @@ def _read_wav(path: Path) -> tuple[bytes, int]:
350350
@staticmethod
351351
def _write_wav(path: Path, pcm: bytes, sample_rate: int) -> None:
352352
"""Write mono 16-bit PCM to a WAV file."""
353-
with wave.open(str(path), "wb") as wf:
354-
wf.setnchannels(1)
355-
wf.setsampwidth(2)
356-
wf.setframerate(sample_rate)
357-
wf.writeframes(pcm)
353+
path.write_bytes(pcm_to_wav(pcm, sample_rate))

src/pipecat/evals/transport.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545

4646
import asyncio
4747
import io
48-
import wave
4948
from collections.abc import Awaitable, Callable
5049
from pathlib import Path
5150
from urllib.parse import parse_qs, urlsplit
@@ -54,6 +53,7 @@
5453
from loguru import logger
5554
from PIL import Image
5655

56+
from pipecat.audio.utils import pcm_to_wav
5757
from pipecat.frames.frames import (
5858
CancelFrame,
5959
EndFrame,
@@ -111,17 +111,12 @@ async def _write_wav(path: str, audio: bytes, sample_rate: int, num_channels: in
111111
Encodes the WAV in memory, then writes it to disk without blocking the event
112112
loop.
113113
"""
114-
buffer = io.BytesIO()
115-
with wave.open(buffer, "wb") as wf:
116-
wf.setnchannels(num_channels)
117-
wf.setsampwidth(2)
118-
wf.setframerate(sample_rate)
119-
wf.writeframes(audio)
114+
wav = pcm_to_wav(audio, sample_rate, num_channels)
120115

121116
p = Path(path)
122117
p.parent.mkdir(parents=True, exist_ok=True)
123118
async with aiofiles.open(p, "wb") as f:
124-
await f.write(buffer.getvalue())
119+
await f.write(wav)
125120
logger.info(f"Eval recording saved: {p} ({len(audio)} bytes)")
126121

127122

src/pipecat/processors/aggregators/llm_context.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import base64
1919
import copy
2020
import io
21-
import wave
2221
from collections.abc import Callable
2322
from dataclasses import dataclass
2423
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast, overload
@@ -29,6 +28,7 @@
2928
from pipecat.adapters.schemas.direct_function import DirectFunction
3029
from pipecat.adapters.schemas.function_schema import FunctionSchema
3130
from pipecat.adapters.schemas.tools_schema import ToolsSchema
31+
from pipecat.audio.utils import pcm_to_wav
3232
from pipecat.frames.frames import AudioRawFrame
3333

3434
# The sentinel is part of LLMContext's public surface — tools and tool_choice
@@ -191,15 +191,8 @@ def encode_audio():
191191

192192
data = b"".join(frame.audio for frame in audio_frames)
193193

194-
with io.BytesIO() as buffer:
195-
with wave.open(buffer, "wb") as wf:
196-
wf.setsampwidth(2)
197-
wf.setnchannels(num_channels)
198-
wf.setframerate(sample_rate)
199-
wf.writeframes(data)
200-
201-
encoded_audio = base64.b64encode(buffer.getvalue()).decode("utf-8")
202-
return encoded_audio
194+
wav = pcm_to_wav(data, sample_rate, num_channels)
195+
return base64.b64encode(wav).decode("utf-8")
203196

204197
encoded_audio = await asyncio.to_thread(encode_audio)
205198

src/pipecat/services/stt_service.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@
77
"""Base classes for Speech-to-Text services with continuous and segmented processing."""
88

99
import asyncio
10-
import io
1110
import time
1211
import warnings
13-
import wave
1412
from abc import abstractmethod
1513
from collections.abc import AsyncGenerator
1614
from typing import Any
1715

1816
from loguru import logger
1917
from websockets.protocol import State
2018

19+
from pipecat.audio.utils import pcm_to_wav
2120
from pipecat.frames.frames import (
2221
AudioRawFrame,
2322
CancelFrame,
@@ -879,15 +878,7 @@ async def _handle_user_stopped_speaking(self, frame: VADUserStoppedSpeakingFrame
879878
await self.emit_stt_usage_metrics()
880879

881880
if self.wants_wav_segments:
882-
content = io.BytesIO()
883-
wav = wave.open(content, "wb")
884-
wav.setsampwidth(2)
885-
wav.setnchannels(1)
886-
wav.setframerate(self.sample_rate)
887-
wav.writeframes(self._audio_buffer)
888-
wav.close()
889-
content.seek(0)
890-
audio = content.read()
881+
audio = pcm_to_wav(self._audio_buffer, self.sample_rate)
891882
else:
892883
# Local models read the buffer as raw 16-bit PCM; wrapping it in a
893884
# WAV container would make them misread the 44-byte header as audio.

src/pipecat/transports/websocket/client.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@
1212
"""
1313

1414
import asyncio
15-
import io
1615
import time
17-
import wave
1816
from collections.abc import Awaitable, Callable
1917

2018
import websockets
2119
from loguru import logger
2220
from pydantic.main import BaseModel
2321
from websockets.asyncio.client import connect as websocket_connect
2422

23+
from pipecat.audio.utils import pcm_to_wav
2524
from pipecat.frames.frames import (
2625
CancelFrame,
2726
EndFrame,
@@ -436,18 +435,11 @@ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
436435
)
437436

438437
if self._params.add_wav_header:
439-
with io.BytesIO() as buffer:
440-
with wave.open(buffer, "wb") as wf:
441-
wf.setsampwidth(2)
442-
wf.setnchannels(frame.num_channels)
443-
wf.setframerate(frame.sample_rate)
444-
wf.writeframes(frame.audio)
445-
wav_frame = OutputAudioRawFrame(
446-
buffer.getvalue(),
447-
sample_rate=frame.sample_rate,
448-
num_channels=frame.num_channels,
449-
)
450-
frame = wav_frame
438+
frame = OutputAudioRawFrame(
439+
pcm_to_wav(frame.audio, frame.sample_rate, frame.num_channels),
440+
sample_rate=frame.sample_rate,
441+
num_channels=frame.num_channels,
442+
)
451443

452444
await self._write_frame(frame)
453445

src/pipecat/transports/websocket/fastapi.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@
1212
"""
1313

1414
import asyncio
15-
import io
1615
import time
1716
import typing
18-
import wave
1917
from collections.abc import Awaitable, Callable
2018

2119
from loguru import logger
2220
from pydantic import BaseModel, Field
2321

22+
from pipecat.audio.utils import pcm_to_wav
2423
from pipecat.frames.frames import (
2524
CancelFrame,
2625
ClientConnectedFrame,
@@ -535,18 +534,11 @@ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
535534
)
536535

537536
if self._params.add_wav_header:
538-
with io.BytesIO() as buffer:
539-
with wave.open(buffer, "wb") as wf:
540-
wf.setsampwidth(2)
541-
wf.setnchannels(frame.num_channels)
542-
wf.setframerate(frame.sample_rate)
543-
wf.writeframes(frame.audio)
544-
wav_frame = OutputAudioRawFrame(
545-
buffer.getvalue(),
546-
sample_rate=frame.sample_rate,
547-
num_channels=frame.num_channels,
548-
)
549-
frame = wav_frame
537+
frame = OutputAudioRawFrame(
538+
pcm_to_wav(frame.audio, frame.sample_rate, frame.num_channels),
539+
sample_rate=frame.sample_rate,
540+
num_channels=frame.num_channels,
541+
)
550542

551543
await self._write_frame(frame)
552544

src/pipecat/transports/websocket/server.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
"""
1313

1414
import asyncio
15-
import io
1615
import time
17-
import wave
1816
from collections.abc import Awaitable, Callable
1917
from typing import cast
2018

@@ -25,6 +23,7 @@
2523
from websockets.protocol import State
2624
from websockets.typing import Origin
2725

26+
from pipecat.audio.utils import pcm_to_wav
2827
from pipecat.frames.frames import (
2928
CancelFrame,
3029
ClientConnectedFrame,
@@ -460,18 +459,11 @@ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
460459
)
461460

462461
if self._params.add_wav_header:
463-
with io.BytesIO() as buffer:
464-
with wave.open(buffer, "wb") as wf:
465-
wf.setsampwidth(2)
466-
wf.setnchannels(frame.num_channels)
467-
wf.setframerate(frame.sample_rate)
468-
wf.writeframes(frame.audio)
469-
wav_frame = OutputAudioRawFrame(
470-
buffer.getvalue(),
471-
sample_rate=frame.sample_rate,
472-
num_channels=frame.num_channels,
473-
)
474-
frame = wav_frame
462+
frame = OutputAudioRawFrame(
463+
pcm_to_wav(frame.audio, frame.sample_rate, frame.num_channels),
464+
sample_rate=frame.sample_rate,
465+
num_channels=frame.num_channels,
466+
)
475467

476468
await self._write_frame(frame)
477469

0 commit comments

Comments
 (0)