You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Twilio Media Stream经WebSocket保存为文件后杂音严重问题排查

解决Twilio WebSocket音频保存为WAV后杂音严重的问题

问题根源

Twilio Media Stream传输的音频是PCMU(μ-law)压缩编码格式,这是电话网络常用的窄带压缩标准。你的代码直接将这种压缩后的字节数据写入WAV文件,但WAV容器默认需要未压缩的16位PCM音频数据。播放软件把PCMU编码当成原始PCM解析,就会出现严重杂音。

解决方案

使用Python标准库audioop将PCMU解码为16位PCM,再写入WAV文件。以下是修改后的完整代码:

# Program to accept a phone call via Twilio and save what the speaker says to a WAVE file
# Need to have ngrok, FastAPI and Twilio all setup properly

import os
import json
import base64
import wave
import audioop  # 新增:用于PCMU解码
from fastapi import FastAPI, WebSocket, Request, WebSocketDisconnect
from fastapi.responses import HTMLResponse, Response
from jinja2 import Template

app = FastAPI()

# Global variables
wsserver = []

# Set the filename for writing the media stream
output_filename = "output.wav"

# 修改:存储解码后的16位PCM数据
pcm_data = bytearray()

# Default values, will be overriden by Twilio's start message
channels = 1  # Mono
sample_width = 2  # 修改:16位PCM是2字节
frame_rate = 8000  # PCMU标准采样率

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    global wsserver
    await websocket.accept()
    wsserver.append(websocket)
    while True:
        message = await websocket.receive_text()
        await on_message(websocket, message)

async def on_message(websocket, message):
    global wsserver
    global frame_rate
    global channels
    global pcm_data
    global sample_width

    try:
        msg = json.loads(message)
        event = msg.get("event")

        if event == "connected":
            print("A new call has connected.")

        elif event == "start":
            print(f"Starting Media Stream {msg.get('streamSid')}")
            print(msg)

            # Override values with Twilio's media format info
            channels = msg['start']['mediaFormat']['channels']
            frame_rate = msg['start']['mediaFormat']['sampleRate']

        elif event == "media":
            payload = msg['media']['payload']
        
            if payload:
                # Decode base64-encoded PCMU data
                pcmu_bytes = base64.b64decode(payload)
                # 关键:将PCMU解码为16位小端PCM
                # ulaw2lin参数:输入数据, 输入采样宽度(1字节PCMU), 输出采样宽度(2字节16位PCM)
                pcm_bytes = audioop.ulaw2lin(pcmu_bytes, 1, 2)
                # 添加到PCM缓冲区
                pcm_data.extend(pcm_bytes)

        elif event == "stop":
            print("Call Has Ended")

            # 计算PCM帧数量(每个帧是sample_width字节)
            frames = len(pcm_data) // sample_width

            # 写入WAV文件
            with wave.open(output_filename, 'w') as wav_file:
                wav_file.setnchannels(channels)
                wav_file.setsampwidth(sample_width)
                wav_file.setframerate(frame_rate)
                wav_file.setnframes(frames)
                wav_file.writeframes(pcm_data)

    except WebSocketDisconnect as e:
        print(f"WebSocket disconnected: {e}")
        wsserver.remove(websocket)


@app.post("/")
async def post(request: Request):
    host = request.client.host
    print("Post call - host=" + host)
    xml = Template("""
    <Response>
        <Start>
            <Stream url="wss://83b4-73-70-107-57.ngrok-free.app/ws"/>
        </Start>
        <Say>Please state your message</Say>
        <Pause length="60" />
    </Response>
    """).render(host=host)
    return Response(content=xml, media_type="text/xml")

if __name__ == "__main__":
    print("Listening at Port 8080")
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8080)

关键修改点

  1. 新增audioop库导入,用于PCMU到PCM的解码
  2. 将缓冲区从pcmu_data改为pcm_data,存储解码后的16位PCM数据
  3. 修改sample_width为2(对应16位PCM)
  4. 在media事件处理中,用audioop.ulaw2lin将PCMU字节解码为16位PCM字节
  5. 写入WAV文件时,使用解码后的PCM数据,并正确计算帧数量

内容的提问来源于stack exchange,提问作者brentlyjr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 12:32:32