收藏
回答

tts流式返回的mp3音频片段,播放不流畅有什么好的解决办法吗?

发给后端文本,使用chunk流式返回mp3音频片段。使用audioContext.decodeAudioData解码后,使用audioContext.createBufferSource播放。

虽然能够播放,但是片段与片段之间的播放不流畅,一顿一顿的。尝试过缓存一段播放,衔接处也是不流畅。有什么好的解决方案吗?

/**
 * MP3 流式播放器
 * 用于在 Taro 小程序中使用 WebAudioContext 实现 MP3 格式的音频流式播放
 */
import Taro, { WebAudioContext } from '@tarojs/taro';


// 播放器配置接口
export interface AudioStreamPlayerOptions {
  inputCodec?: string; // 输入编码格式
  channels?: number; // 声道数
  sampleRate?: number; // 采样率
  flushTime?: number; // 刷新时间
  volume?: number; // 音量 (0-1)
  onended?: (e: any) => void; // 播放结束回调
  onload?: () => void; // 音频加载完成回调
  onplay?: () => void; // 开始播放回调
  onpause?: () => void; // 暂停播放回调
  onerror?: (err: any) => void; // 播放错误回调
}


interface AudioData {
  data: ArrayBuffer;
  isLastChunk: boolean;
}


interface AudioDecodedData {
  buffer: AudioBuffer;
  isLastChunk: boolean;
}



/**
 * MP3 流式播放器类
 * 支持接收 ArrayBuffer 格式的 MP3 数据流并实时播放
 * 使用 WebAudioContext 实现音频解码和播放
 */
export class AudioStreamPlayer {
  private options: Required<AudioStreamPlayerOptions>;
  private audioContext: WebAudioContext | null = null; // WebAudioContext 实例
  private audioQueue: AudioData[] = []; // 音频数据队列
  private isInitialized = false;
  private flushInterval: number | null = null;
  private audioSource: AudioBufferSourceNode | null = null; // 音频源节点
  private gainNode: GainNode | null = null; // 音量控制节点
  private audioBufferQueue: AudioDecodedData[] = []; // 解码后的音频缓冲区队列
  private isDecoding = false; // 是否正在解码
  private startTime = 0;
  private isPlaying = false;


  constructor(options: AudioStreamPlayerOptions) {
    // 设置默认配置
    this.options = {
      inputCodec: options.inputCodec || 'mp3',
      channels: options.channels || 1,
      sampleRate: options.sampleRate || 16000,
      flushTime: options.flushTime || 200,
      volume: options.volume || 1,
      onended: options.onended || ((isLast: boolean) => { }),
      onload: options.onload || (() => { }),
      onplay: options.onplay || (() => { }),
      onpause: options.onpause || (() => { }),
      onerror: options.onerror || ((err) => console.error('音频播放错误:', err))
    };


    this.initAudioContext();
  }


  /**
   * 初始化 WebAudioContext
   */
  private initAudioContext() {
    try {
      // 使用 TarocreateWebAudioContext API
      this.audioContext = Taro.createWebAudioContext();


      // 创建音量控制节点
      this.gainNode = this.audioContext.createGain();
      this.gainNode.gain.value = this.options.volume;


      // 连接节点: 音量节点 -> 输出
      this.gainNode.connect(this.audioContext.destination);
      this.startTime = this.audioContext.currentTime;
      this.isInitialized = true;
      this.options.onload && this.options.onload();


      console.log('WebAudioContext 初始化成功');
    } catch (error) {
      console.error('初始化 WebAudioContext 失败:', error);
      this.options.onerror && this.options.onerror(error);
    }
  }


  /**
   * 设置数据刷新定时器
   */
  private setupFlushInterval() {
    this.flushInterval = setInterval(() => {
      this.flush();
    }, this.options.flushTime) as unknown as number;
  }


  /**
   * 解码并播放音频数据
   */
  private async flush() {
    console.log('flush', {
      isInitialized: this.isInitialized,
      isDecoding: this.isDecoding,
      isPlaying: this.isPlaying,
      audioQueueLength: this.audioQueue.length,
      audioBufferQueueLength: this.audioBufferQueue.length
    });
    if (!this.isInitialized || this.isDecoding) {
      return;
    }
    this.isDecoding = true;
    // 将audioQueue中的数据合并成一个buffer
    if (this.audioQueue.length === 0) {
      this.isDecoding = false;
      return;
    }


    /*  const tempAudioQueue = this.audioQueue.splice(0, this.audioQueue.length);
     console.log('flush tempAudioQueue', JSON.parse(JSON.stringify(tempAudioQueue)))
     const isLastChunk = tempAudioQueue[tempAudioQueue.length - 1].isLastChunk;
     // 计算所有音频数据的总长度
     const totalLength = tempAudioQueue.reduce((acc, current) => acc + current.data.byteLength, 0);
     const combinedBuffer = new Uint8Array(totalLength);
     let offset = 0;
 
     // 将所有音频数据合并到 combinedBuffer 中
     while (tempAudioQueue.length > 0) {
       const audioData = tempAudioQueue.shift()!;
       combinedBuffer.set(new Uint8Array(audioData.data), offset);
       offset += audioData.data.byteLength;
     } */


    const audioData = this.audioQueue.shift()!;
    try {
      // 解码音频数据
      await this.decodeAudioData(audioData);
      if (!this.isPlaying) {
        this.isPlaying = true;
        this.playNextBuffer();
      }
    } catch (error) {
      console.error('音频数据失败:', error);
      if (audioData.isLastChunk) {
        this.options.onended && this.options.onended(true)
        this.stop();
      }
    } finally {
      this.isDecoding = false;
    }
  }


  /**
   * 解码音频数据
   * @param audioData 原始音频数据
   */
  private async decodeAudioData(audioData: AudioData) {
    return new Promise<void>((resolve, reject) => {
      if (audioData.isLastChunk) {
        this.audioBufferQueue.push({ buffer: null, isLastChunk: true });
        resolve();
        return;
      }
      this.audioContext?.decodeAudioData(audioData.data,
        (buffer: AudioBuffer) => {
          // 解码成功,将音频缓冲区添加到队列
          this.audioBufferQueue.push({ buffer, isLastChunk: audioData.isLastChunk });
          resolve();
        },
        (error: any) => {
          reject(error);
        }
      );


    });
  }


  /**
   * 播放下一个音频缓冲区
   */
  private playNextBuffer() {
    console.log('playNextBuffer', this.audioBufferQueue.length);


    if (this.audioBufferQueue.length === 0) {
      return;
    }
    const bufferData = this.audioBufferQueue.shift();


    try {
      // 获取下一个音频缓冲区
      if (bufferData?.isLastChunk) {
        this.options.onended && this.options.onended(true)
        this.stop();
        return;
      }
      const buffer = bufferData.buffer;
      console.log('duration', buffer.duration)
      // 创建音频源节点
      this.audioSource = this.audioContext.createBufferSource();
      this.audioSource.buffer = buffer;



      // 播放结束事件
      this.audioSource.onended = () => {
        // 播放结束,如果还有缓冲,继续播放
        console.log('onended', new Date().getTime())
        this.audioSource.disconnect();
        if (bufferData.isLastChunk) {
          console.warn('最后一个');
          this.isPlaying = false;
          this.options.onended && this.options.onended(true)
          this.stop()
          return;
        } else {
          this.playNextBuffer();
        }
      };
      // 连接节点: 音频源 -> 音量节点
      this.audioSource.connect(this.gainNode);


      // 开始播放
      this.audioSource.start(this.startTime);
      this.startTime += buffer.duration;
    } catch (error) {
      this.isPlaying = false;
      console.error('播放音频缓冲区失败:', error);


    }
  }


  /**
   * 接收音频数据
   * @param data ArrayBuffer 格式的音频数据
   */
  feed(audioData: AudioData) {
    console.log('feed', audioData.data.byteLength, audioData.isLastChunk);


    // 将数据添加到队列
    this.audioQueue.push(audioData);
  }


  /**
   * 设置音量
   * @param volume 音量值 (0-1)
   */
  volume(volume: number) {
    this.options.volume = Math.max(0, Math.min(1, volume));
    if (this.gainNode) {
      this.gainNode.gain.value = this.options.volume;
    }
  }


  /**
   * 暂停播放
   */
  pause() {
    this.options.onpause && this.options.onpause();
  }


  /**
   * 恢复播放
   */
  play() {


    this.options.onplay && this.options.onplay();
    this.setupFlushInterval();


    // 如果有解码后的音频缓冲区,开始播放
    // this.playNextBuffer();
  }


  /**
   * 停止播放并清空数据
   */
  stop() {
    if (this.flushInterval) {
      clearInterval(this.flushInterval);
      this.flushInterval = null;
    }
    // 停止当前播放的音频
    if (this.audioSource) {
      try {
        this.audioSource.stop();
      } catch (e) {
        // 忽略已停止的错误
      }
      this.audioSource = null;
    }


    this.isPlaying = false;
    this.audioQueue = [];
    this.audioBufferQueue = [];
  }


  /**
   * 销毁播放器
   */
  destroy() {
    console.log('destroy销毁播放器', this.flushInterval);
    // 清除定时器
    if (this.flushInterval) {
      clearInterval(this.flushInterval);
      this.flushInterval = null;
    }


    // 停止并释放音频资源
    this.stop();


    // 释放音频上下文
    if (this.audioContext) {
      try {
        this.audioContext.close();
      } catch (e) {
        console.error('关闭音频上下文失败:', e);
      }
      this.audioContext = null;
    }


    // 清空引用
    this.gainNode = null;
    this.isInitialized = false;
  }
}


export default AudioStreamPlayer;


回答关注问题邀请回答
收藏

2 个回答

  • 再见,电脑崽
    再见,电脑崽
    2025-12-19

    你可以试试这个插件:RealTimeTTS

    2025-12-19
    有用 1
    回复
  • 智能回答 智能回答 本次回答由AI生成
    2025-12-02
    有用
登录 后发表内容