入站出站音频存入共享缓冲区后失真延迟的原因及解决办法
解决方案:两路音频轨道的正确合并与存储
问题根源
你直接将入站、出站的音频chunk无序拼接到shared缓冲区的做法是错误的。WebSocket收到的媒体事件是异步触发的,入站和出站的音频帧并非严格按时间顺序交替到达,导致shared里的音频数据时序完全混乱,最终播放时出现卡顿、变形。而独立缓冲区只存储单轨道的连续帧,所以播放正常;简单平均合并是基于同时间点的帧混合,因此也能正常播放。
可行方案
方案1:时间戳对齐后混合为单轨音频
如果你的转录模型只支持单轨输入,需要先对两路音频按时间戳对齐,再混合同时间点的音频帧:
- 给每个收到的音频chunk记录对应的时间戳(Twilio媒体事件里的
timestamp字段可直接获取) - 分别维护两路音频的「时间戳-帧数据」队列
- 定期从两个队列中取出时间重叠的帧,进行混合(比如取两帧的平均值),再写入共享缓冲区
示例代码:
from collections import deque import audioop import base64 obuffer_frames = deque() # 存储(时间戳, 帧数据) ibuffer_frames = deque() shared = b"" INBOUND = "inbound" OUTBOUND = "outbound" while True: data = await queue.get() if data["event"] == "media": websocket_payload = data["media"]["payload"] chunk = audioop.ulaw2lin(base64.b64decode(websocket_payload), 2) timestamp = data["media"]["timestamp"] # 获取Twilio提供的时间戳 track = data["media"]["track"] if track == INBOUND: obuffer_frames.append((timestamp, chunk)) elif track == OUTBOUND: ibuffer_frames.append((timestamp, chunk)) # 尝试对齐并混合帧 while obuffer_frames and ibuffer_frames: ts_out, frame_out = obuffer_frames[0] ts_in, frame_in = ibuffer_frames[0] # 取时间最早的帧作为基准,处理重叠部分 if ts_out <= ts_in: frame_len = len(frame_out) overlap_len = min(len(frame_in), frame_len) # 混合16位PCM帧,缩放避免溢出 mixed_frame = audioop.add(frame_out[:overlap_len], frame_in[:overlap_len], 2) mixed_frame = audioop.mul(mixed_frame, 2, 0.5) shared += mixed_frame # 更新队列:移除已处理的帧或截断剩余部分 if len(frame_out) == overlap_len: obuffer_frames.popleft() else: obuffer_frames[0] = (ts_out, frame_out[overlap_len:]) if len(frame_in) == overlap_len: ibuffer_frames.popleft() else: ibuffer_frames[0] = (ts_in, frame_in[overlap_len:]) else: # 处理入站帧更早的情况,逻辑对称 frame_len = len(frame_in) overlap_len = min(len(frame_out), frame_len) mixed_frame = audioop.add(frame_in[:overlap_len], frame_out[:overlap_len], 2) mixed_frame = audioop.mul(mixed_frame, 2, 0.5) shared += mixed_frame if len(frame_in) == overlap_len: ibuffer_frames.popleft() else: ibuffer_frames[0] = (ts_in, frame_in[overlap_len:]) if len(frame_out) == overlap_len: obuffer_frames.popleft() else: obuffer_frames[0] = (ts_out, frame_out[overlap_len:])
方案2:存储为立体声多轨音频
如果你的转录模型支持多轨输入,无需混合,直接将两路音频分别作为左右声道存储为立体声WAV:
- 维护两个独立的单轨缓冲区(即你原本的
obuffer和ibuffer) - 定期将两路缓冲区中相同长度的帧交错拼接成立体声音频
- 将拼接后的立体声数据写入共享缓冲区或直接提供给转录模型
示例代码(修改序列化保存部分):
import wave # 确保两路缓冲区长度一致,截断到较短的长度 min_len = min(len(obuffer), len(ibuffer)) stereo_data = b"" # 按采样点交替拼接:左声道入站,右声道出站 for i in range(0, min_len, 2): stereo_data += obuffer[i:i+2] + ibuffer[i:i+2] # 保存为立体声WAV nchannels = 2 # 双声道 sampwidth = 2 framerate = 8000 nframes = min_len // (1 * sampwidth) # 单轨的总帧数 with wave.open("stereo_audio.wav", 'wb') as wf: wf.setnchannels(nchannels) wf.setsampwidth(sampwidth) wf.setframerate(framerate) wf.setnframes(nframes) wf.writeframes(stereo_data)
关键注意事项
- 必须依赖Twilio媒体事件中的
timestamp字段做时间对齐,否则无法保证两路音频的时序正确性 - 混合音频时要通过缩放避免PCM数据溢出,防止出现爆音或失真
- 如果转录模型支持多轨输入,优先选择方案2,避免混合带来的音质损失
内容的提问来源于stack exchange,提问作者Sam Comber
相关产品推荐
相关产品推荐

