发给后端文本,使用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 {
// 使用 Taro 的 createWebAudioContext 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;

你可以试试这个插件:RealTimeTTS