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

入站出站音频存入共享缓冲区后失真延迟的原因及解决办法

解决方案:两路音频轨道的正确合并与存储

问题根源

你直接将入站、出站的音频chunk无序拼接到shared缓冲区的做法是错误的。WebSocket收到的媒体事件是异步触发的,入站和出站的音频帧并非严格按时间顺序交替到达,导致shared里的音频数据时序完全混乱,最终播放时出现卡顿、变形。而独立缓冲区只存储单轨道的连续帧,所以播放正常;简单平均合并是基于同时间点的帧混合,因此也能正常播放。

可行方案

方案1:时间戳对齐后混合为单轨音频

如果你的转录模型只支持单轨输入,需要先对两路音频按时间戳对齐,再混合同时间点的音频帧:

  1. 给每个收到的音频chunk记录对应的时间戳(Twilio媒体事件里的timestamp字段可直接获取)
  2. 分别维护两路音频的「时间戳-帧数据」队列
  3. 定期从两个队列中取出时间重叠的帧,进行混合(比如取两帧的平均值),再写入共享缓冲区

示例代码:

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:

  1. 维护两个独立的单轨缓冲区(即你原本的obuffer和ibuffer)
  2. 定期将两路缓冲区中相同长度的帧交错拼接成立体声音频
  3. 将拼接后的立体声数据写入共享缓冲区或直接提供给转录模型

示例代码(修改序列化保存部分):

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 14:01:05