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

Azure TTS流式传输音频失真问题排查求助

Azure TTS流式传输音频后期失真问题排查与解决

我用JavaScript的microsoft-cognitiveservices-speech-sdk中SpeechSynthesizer生成Audio16Khz32KBitRateMonoMp3格式音频,通过Express流式传输到React前端时,前几句音频正常,但后面的语音严重失真。

发送音频的代码

synthesizer.synthesizing = function (s, e) {
  currentAudioChunk = {
    audio: Buffer.from(e.result.audioData),
    offset: e.result.audioDuration / 10000, // 转换为毫秒
  };

  sendEvent("audioData", {
    audio: currentAudioChunk.audio.toString("base64"),
    // 目前audioOffset数据为null,暂用占位符
    audioOffset: "0",
  });

  currentAudioChunk = null;
};

音频数据对比

  • 正常时的base64片段:
//NIxCElU/5IAY+IAYJ/2sMoSxHk0RH9BVzA0GTFmDQIAQ4Xv8iBn5oEqAUwtPDlBnyDh9f+ukXFoLdRAhSgoYRmQwQAIIJ0DVH/+eez/FwFkR+DcgYghGI/ImQQ0kU/////LRuTiDFw8fLRmk9Bv////v/+hTTN0Cuzk4TZ8qGCFf6UeMhhgB8BjVMd/t5V//NIxA4g2tqkAc9YAD7MqNv/AVisORkvL3sJA7Fje5e+5N7r/ZNxXO9991vl98n9A+bqGiY3myJPJAOEDjHGJfRMWtAsfGoO9ejd0GhoOy32zfEvfHvZL4vfvTvfV5xO2TeymMriv6//j+v5/j2MNzc+w4oEElx7v6u/2t/UcW0Tg+oDGEGSAs30kiaCvEsa//NIxA0f6tbFtGsQsB/wSwiDZ+tZaEobTf5rXCqf/XqycoAzWXtvSKlDpriVZX/mmfM+YFhrAo1ookTMDgiGJRKHii37Q6Eh+fFkyMUXF4c2/fTr+0rpvsyXQevBDueX8PPz/PPX88VXXxySFgZQJQqJRW71f/Ss+4XRpRipAO1XGSKjcylABX/VcJmGEBBG
  • 失真后的base64片段(含大量重复无效字符):
//NIxHwAAANIAAAAAFVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NIxHwAAANIAAAAAFVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NIxHwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV

这些重复的V字符是从Azure接收的原始数据中就存在的,不是字符串转换导致的,直接删除会损坏音频数据。


解决方法

1. 切换音频格式,降低流式解析压力

MP3是带帧结构的压缩格式,流式传输时容易因不完整帧导致失真。可以先使用Audio16Khz16BitMonoPcm格式(无压缩的PCM),传输到前端后再转码为MP3,避免帧不完整的问题。修改合成器初始化代码:

const speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region);
speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio16Khz16BitMonoPcm;

2. 缓存并校验完整MP3帧

如果坚持使用MP3格式,需要缓存音频块,直到收集到完整的MP3帧再发送。MP3帧以同步字(通常是0xFFFB)开头,我们可以通过检测同步字来分割完整帧:

// 缓存未完成的MP3帧
let incompleteFrame = Buffer.from([]);

synthesizer.synthesizing = function (s, e) {
  let audioChunk = Buffer.from(e.result.audioData);
  // 合并缓存的不完整帧和当前块
  let combined = Buffer.concat([incompleteFrame, audioChunk]);
  
  // 查找最后一个完整帧的起始位置
  let syncWord = Buffer.from([0xFF, 0xFB]);
  let lastFrameStart = combined.lastIndexOf(syncWord);
  
  if (lastFrameStart > 0) {
    // 提取完整帧发送
    let completeFrames = combined.slice(0, lastFrameStart);
    sendEvent("audioData", {
      audio: completeFrames.toString("base64"),
      audioOffset: (e.result.audioDuration / 10000).toString()
    });
    // 缓存剩余的不完整部分
    incompleteFrame = combined.slice(lastFrameStart);
  } else {
    // 没有找到完整帧,全部缓存
    incompleteFrame = combined;
  }
};

// 合成结束时发送剩余缓存的帧
synthesizer.synthesisCompleted = function (s, e) {
  if (incompleteFrame.length > 0) {
    sendEvent("audioData", {
      audio: incompleteFrame.toString("base64"),
      audioOffset: "0"
    });
    incompleteFrame = Buffer.from([]);
  }
};

3. 修正音频偏移量

当前代码中audioOffset用固定值"0",会导致前端拼接音频时时序混乱。应该使用e.result.audioDuration计算正确的偏移量,让前端按顺序拼接音频块:

sendEvent("audioData", {
  audio: currentAudioChunk.audio.toString("base64"),
  audioOffset: (e.result.audioDuration / 10000).toString()
});

4. 检查网络传输稳定性

流式传输中丢包或数据截断也会导致失真,可以在传输时添加简单的校验(比如每个块附带长度信息),前端收到后验证长度是否匹配,确保数据完整。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 18:53:20