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)
关键修改点
- 新增
audioop库导入,用于PCMU到PCM的解码 - 将缓冲区从
pcmu_data改为pcm_data,存储解码后的16位PCM数据 - 修改
sample_width为2(对应16位PCM) - 在
media事件处理中,用audioop.ulaw2lin将PCMU字节解码为16位PCM字节 - 写入WAV文件时,使用解码后的PCM数据,并正确计算帧数量
内容的提问来源于stack exchange,提问作者brentlyjr
相关产品推荐
相关产品推荐

