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

将OpenAI TTS流式音频实时发送至Twilio WebSocket遇杂音问题

问题分析与解决方案

核心问题出在对OpenAI流式WAV数据的错误处理,以及零散音频块转码的误差积累,具体原因和修复方案如下:

关键问题点

  1. 错误的WAV块处理:OpenAI返回的流式WAV是单个完整WAV文件的流式输出——第一个data块包含完整文件头+部分PCM数据,后续所有块都是纯PCM采样数据。你给每个块单独套WAV头,相当于把每个PCM片段当成独立WAV文件处理,破坏了采样的连续性,直接导致转码后音频断裂、杂音。
  2. 零散块转码误差:采样率转换和mu-law编码算法依赖连续的上下文采样数据,极小的零散块单独转码会引入大量截断误差,表现为杂音。
  3. 未对齐Twilio媒体块要求:Twilio WebSocket要求媒体块大小对齐到电话系统标准帧(通常20ms,对应8kHz采样下160字节mu-law数据),零散块发送会导致播放异常。

修复后的代码实现

import { WaveFile } from 'wavefile';

async function streamOpenAITTS(text, ws, streamSid) {
  const response = await openai.audio.speech.create({
    model: "tts-1",
    voice: "alloy",
    input: text,
    response_format: "wav",
  });

  let wavHeaderParsed = false;
  let wavProcessor = null;
  let pcmBuffer = Buffer.alloc(0);
  // 每块对应20ms音频:8kHz采样率 * 0.02s = 160样本,16位PCM每个样本2字节 → 320字节PCM数据
  const targetPCMBlockSize = 8000 * 0.02 * 2;

  response.body.on("data", (chunk) => {
    if (!wavHeaderParsed) {
      // 第一次接收的块包含完整WAV头,先解析采样参数
      wavProcessor = new WaveFile(chunk);
      wavHeaderParsed = true;
      // 提取文件头之后的纯PCM数据
      const pcmStart = wavProcessor.containerSize;
      pcmBuffer = Buffer.concat([pcmBuffer, chunk.slice(pcmStart)]);
    } else {
      // 后续块都是纯PCM数据,直接追加到缓存
      pcmBuffer = Buffer.concat([pcmBuffer, chunk]);
    }

    // 当缓存的PCM数据足够处理一个标准块时,批量转码发送
    while (pcmBuffer.length >= targetPCMBlockSize) {
      const processChunk = pcmBuffer.slice(0, targetPCMBlockSize);
      pcmBuffer = pcmBuffer.slice(targetPCMBlockSize);

      // 复用已解析的WAV参数,写入待处理的PCM数据
      wavProcessor.data.samples = new Int16Array(processChunk.buffer);
      
      // 转采样到8kHz → 转mu-law编码
      wavProcessor.toSampleRate(8000);
      wavProcessor.toMuLaw();

      // 转换为Twilio要求的base64 payload
      const mulawBuffer = Buffer.from(wavProcessor.data.samples);
      const payload = mulawBuffer.toString("base64");

      try {
        ws.send(JSON.stringify({
          event: "media",
          streamSid: streamSid,
          media: { payload }
        }));
      } catch (e) {
        console.error("发送音频块失败:", e);
      }
    }
  });

  // 处理流结束后剩余的不足一个标准块的PCM数据
  response.body.on("end", () => {
    if (pcmBuffer.length > 0) {
      wavProcessor.data.samples = new Int16Array(pcmBuffer.buffer);
      wavProcessor.toSampleRate(8000);
      wavProcessor.toMuLaw();
      
      const mulawBuffer = Buffer.from(wavProcessor.data.samples);
      const payload = mulawBuffer.toString("base64");
      
      try {
        ws.send(JSON.stringify({
          event: "media",
          streamSid: streamSid,
          media: { payload }
        }));
        // 发送结束标记
        ws.send(JSON.stringify({
          event: "mark",
          streamSid: streamSid,
          mark: { name: "response" }
        }));
      } catch (e) {
        console.error("发送最后音频块失败:", e);
      }
    }
  });

  response.body.on("error", (err) => {
    console.error("TTS流错误:", err);
  });
}

核心优化说明

  • 正确解析流式WAV结构:只在第一次接收时解析WAV头,后续直接处理纯PCM数据,保证采样连续性。
  • 批量缓存转码:积累足够的PCM数据(20ms标准帧)再转码,给算法提供足够上下文,减少杂音。
  • 对齐Twilio媒体块:严格按照电话系统标准帧大小发送,避免播放异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 05:45:11