mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-25 08:33:53 +08:00
fix: 🐛 修复test_page.html 播放异常问题
This commit is contained in:
@@ -452,6 +452,16 @@
|
|||||||
let audioBuffers = []; // 用于存储接收到的所有音频数据
|
let audioBuffers = []; // 用于存储接收到的所有音频数据
|
||||||
let totalAudioSize = 0; // 跟踪累积的音频大小
|
let totalAudioSize = 0; // 跟踪累积的音频大小
|
||||||
|
|
||||||
|
let audioBufferQueue = []; // 存储接收到的音频包
|
||||||
|
let isAudioBuffering = false; // 是否正在缓冲音频
|
||||||
|
let isAudioPlaying = false; // 是否正在播放音频
|
||||||
|
const BUFFER_THRESHOLD = 3; // 缓冲包数量阈值,至少累积3个包再开始播放
|
||||||
|
const MIN_AUDIO_DURATION = 0.1; // 最小音频长度(秒),小于这个长度的音频会被合并
|
||||||
|
let streamingContext = null; // 音频流上下文
|
||||||
|
const SAMPLE_RATE = 16000; // 采样率
|
||||||
|
const CHANNELS = 1; // 声道数
|
||||||
|
const FRAME_SIZE = 960; // 帧大小
|
||||||
|
|
||||||
// DOM元素
|
// DOM元素
|
||||||
const connectButton = document.getElementById('connectButton');
|
const connectButton = document.getElementById('connectButton');
|
||||||
const serverUrlInput = document.getElementById('serverUrl');
|
const serverUrlInput = document.getElementById('serverUrl');
|
||||||
@@ -526,221 +536,360 @@
|
|||||||
conversationDiv.scrollTop = conversationDiv.scrollHeight;
|
conversationDiv.scrollTop = conversationDiv.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放Opus音频数据
|
|
||||||
function playOpusData(opusBuffers) {
|
|
||||||
if (!opusBuffers || opusBuffers.length === 0) {
|
|
||||||
log('无效的Opus数据,无法播放', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying) {
|
// 开始音频缓冲过程
|
||||||
// 已经在播放,加入队列
|
function startAudioBuffering() {
|
||||||
audioQueue.push(opusBuffers);
|
if (isAudioBuffering || isAudioPlaying) return;
|
||||||
log(`音频添加到队列,当前队列长度: ${audioQueue.length}`, 'debug');
|
|
||||||
return;
|
isAudioBuffering = true;
|
||||||
}
|
log("开始音频缓冲...", 'info');
|
||||||
|
|
||||||
isPlaying = true;
|
// 先尝试初始化解码器,以便在播放时已准备好
|
||||||
|
initOpusDecoder().catch(error => {
|
||||||
try {
|
log(`预初始化Opus解码器失败: ${error.message}`, 'warning');
|
||||||
// 确保音频上下文存在
|
// 继续缓冲,我们会在播放时再次尝试初始化
|
||||||
if (!audioContext) {
|
});
|
||||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
|
||||||
sampleRate: 16000
|
// 设置超时,如果在一定时间内没有收集到足够的音频包,就开始播放
|
||||||
});
|
setTimeout(() => {
|
||||||
log('创建音频上下文,采样率: 16000Hz', 'debug');
|
if (isAudioBuffering && audioBufferQueue.length > 0) {
|
||||||
|
log(`缓冲超时,当前缓冲包数: ${audioBufferQueue.length},开始播放`, 'info');
|
||||||
|
playBufferedAudio();
|
||||||
}
|
}
|
||||||
|
}, 300); // 300ms超时
|
||||||
// 确保解码器已初始化
|
|
||||||
if (!opusDecoder) {
|
// 监控缓冲进度
|
||||||
try {
|
const bufferCheckInterval = setInterval(() => {
|
||||||
// 检查ModuleInstance是否存在(本地库导出的全局变量)
|
if (!isAudioBuffering) {
|
||||||
if (typeof window.ModuleInstance === 'undefined') {
|
clearInterval(bufferCheckInterval);
|
||||||
if (typeof Module !== 'undefined') {
|
|
||||||
// 尝试使用全局Module
|
|
||||||
window.ModuleInstance = Module;
|
|
||||||
log('使用全局Module作为ModuleInstance', 'info');
|
|
||||||
} else {
|
|
||||||
throw new Error('Opus库未加载,ModuleInstance和Module对象都不存在');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化Opus解码器 - 16kHz采样率,单声道
|
|
||||||
opusDecoder = {
|
|
||||||
channels: 1,
|
|
||||||
rate: 16000,
|
|
||||||
frameSize: 960, // 60ms @ 16kHz = 60 * 16 = 960 samples
|
|
||||||
frameDuration: 60, // 60ms帧时长
|
|
||||||
module: window.ModuleInstance, // 保存模块实例的引用
|
|
||||||
|
|
||||||
// 使用ModuleInstance提供的函数来解码
|
|
||||||
decode_float: function (opusData) {
|
|
||||||
try {
|
|
||||||
const mod = this.module; // 简写引用
|
|
||||||
|
|
||||||
// 打印输入数据的大小
|
|
||||||
log(`正在解码Opus数据,大小: ${opusData.length}字节`, 'debug');
|
|
||||||
|
|
||||||
// 为输入数据分配内存
|
|
||||||
const inputPtr = mod._malloc(opusData.length);
|
|
||||||
mod.HEAPU8.set(opusData, inputPtr);
|
|
||||||
|
|
||||||
// 为输出PCM数据分配内存 (每个样本为float)
|
|
||||||
const pcmSamples = this.frameSize;
|
|
||||||
const pcmPtr = mod._malloc(pcmSamples * 4); // 4字节/float
|
|
||||||
|
|
||||||
// 创建解码器实例
|
|
||||||
const decoderSize = mod._opus_decoder_get_size(this.channels);
|
|
||||||
log(`Opus解码器大小: ${decoderSize}字节`, 'debug');
|
|
||||||
|
|
||||||
const decoderPtr = mod._malloc(decoderSize);
|
|
||||||
const err = mod._opus_decoder_init(decoderPtr, this.rate, this.channels);
|
|
||||||
|
|
||||||
if (err < 0) {
|
|
||||||
throw new Error(`Opus解码器初始化失败: ${err}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解码音频帧
|
|
||||||
const samplesDecoded = mod._opus_decode_float(
|
|
||||||
decoderPtr,
|
|
||||||
inputPtr,
|
|
||||||
opusData.length,
|
|
||||||
pcmPtr,
|
|
||||||
this.frameSize,
|
|
||||||
0 // 不使用FEC
|
|
||||||
);
|
|
||||||
|
|
||||||
if (samplesDecoded < 0) {
|
|
||||||
throw new Error(`Opus解码失败,错误码: ${samplesDecoded}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`解码成功,获得${samplesDecoded}个PCM样本`, 'debug');
|
|
||||||
|
|
||||||
// 从内存中复制PCM数据
|
|
||||||
const pcmData = new Float32Array(samplesDecoded);
|
|
||||||
for (let i = 0; i < samplesDecoded; i++) {
|
|
||||||
pcmData[i] = mod.HEAPF32[pcmPtr / 4 + i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放内存
|
|
||||||
mod._free(inputPtr);
|
|
||||||
mod._free(pcmPtr);
|
|
||||||
mod._free(decoderPtr);
|
|
||||||
|
|
||||||
return pcmData;
|
|
||||||
} catch (err) {
|
|
||||||
log(`解码过程中出错: ${err.message}`, 'error');
|
|
||||||
return new Float32Array(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
log('Opus解码器初始化成功', 'success');
|
|
||||||
} catch (err) {
|
|
||||||
log(`Opus解码器初始化失败: ${err.message},尝试使用简单播放`, 'error');
|
|
||||||
// 回退到简单播放
|
|
||||||
simpleFallbackPlay(opusBuffers);
|
|
||||||
isPlaying = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解码所有Opus帧
|
|
||||||
let pcmData = [];
|
|
||||||
let successCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < opusBuffers.length; i++) {
|
|
||||||
try {
|
|
||||||
const opusBuffer = opusBuffers[i];
|
|
||||||
// 确保这是Uint8Array
|
|
||||||
const opusArray = opusBuffer instanceof Uint8Array ?
|
|
||||||
opusBuffer : new Uint8Array(opusBuffer);
|
|
||||||
|
|
||||||
// 解码Opus帧为PCM数据
|
|
||||||
const decoded = opusDecoder.decode_float(opusArray);
|
|
||||||
if (decoded && decoded.length > 0) {
|
|
||||||
pcmData.push(decoded);
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log(`Opus帧 #${i + 1} 解码错误: ${error.message}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`解码完成,成功: ${successCount}/${opusBuffers.length} 帧`, 'debug');
|
|
||||||
|
|
||||||
if (pcmData.length === 0) {
|
|
||||||
log('没有成功解码的帧,尝试使用简单播放', 'warning');
|
|
||||||
simpleFallbackPlay(opusBuffers);
|
|
||||||
isPlaying = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接所有PCM数据
|
// 当累积了足够的音频包,开始播放
|
||||||
const totalSamples = pcmData.reduce((acc, arr) => acc + arr.length, 0);
|
if (audioBufferQueue.length >= BUFFER_THRESHOLD) {
|
||||||
log(`总共解码出 ${totalSamples} 个PCM样本`, 'debug');
|
clearInterval(bufferCheckInterval);
|
||||||
|
log(`已缓冲 ${audioBufferQueue.length} 个音频包,开始播放`, 'info');
|
||||||
const combinedPCM = new Float32Array(totalSamples);
|
playBufferedAudio();
|
||||||
|
|
||||||
let offset = 0;
|
|
||||||
for (const buffer of pcmData) {
|
|
||||||
combinedPCM.set(buffer, offset);
|
|
||||||
offset += buffer.length;
|
|
||||||
}
|
}
|
||||||
|
}, 50);
|
||||||
// 使用Web Audio API播放PCM数据
|
|
||||||
const audioBuffer = audioContext.createBuffer(1, combinedPCM.length, 16000);
|
|
||||||
audioBuffer.getChannelData(0).set(combinedPCM);
|
|
||||||
|
|
||||||
const source = audioContext.createBufferSource();
|
|
||||||
source.buffer = audioBuffer;
|
|
||||||
source.connect(audioContext.destination);
|
|
||||||
|
|
||||||
source.onended = () => {
|
|
||||||
isPlaying = false;
|
|
||||||
log('音频播放完成', 'debug');
|
|
||||||
|
|
||||||
// 播放队列中的下一个
|
|
||||||
if (audioQueue.length > 0) {
|
|
||||||
const nextBuffers = audioQueue.shift();
|
|
||||||
playOpusData(nextBuffers);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
source.start();
|
|
||||||
log(`开始播放PCM音频,时长: ${(combinedPCM.length / 16000).toFixed(2)}秒`, 'debug');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
log(`播放Opus音频错误: ${error.message},尝试使用简单播放`, 'error');
|
|
||||||
simpleFallbackPlay(opusBuffers);
|
|
||||||
isPlaying = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 简单播放回退方案
|
// 播放已缓冲的音频
|
||||||
function simpleFallbackPlay(opusBuffers) {
|
function playBufferedAudio() {
|
||||||
try {
|
if (isAudioPlaying || audioBufferQueue.length === 0) return;
|
||||||
log('使用简单播放回退方案', 'warning');
|
|
||||||
|
isAudioPlaying = true;
|
||||||
// 创建一个简单的"滴"声作为反馈
|
isAudioBuffering = false;
|
||||||
if (audioContext) {
|
|
||||||
const oscillator = audioContext.createOscillator();
|
// 确保Opus解码器已初始化
|
||||||
oscillator.type = 'sine';
|
const initDecoderAndPlay = async () => {
|
||||||
oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // A4音
|
try {
|
||||||
|
// 确保音频上下文存在
|
||||||
const gainNode = audioContext.createGain();
|
if (!audioContext) {
|
||||||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); // 设置音量为0.1
|
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
|
sampleRate: SAMPLE_RATE
|
||||||
oscillator.connect(gainNode);
|
});
|
||||||
gainNode.connect(audioContext.destination);
|
log('创建音频上下文,采样率: ' + SAMPLE_RATE + 'Hz', 'debug');
|
||||||
|
}
|
||||||
oscillator.start();
|
|
||||||
oscillator.stop(audioContext.currentTime + 0.2); // 播放0.2秒
|
// 确保解码器已初始化
|
||||||
|
if (!opusDecoder) {
|
||||||
log('播放提示音作为音频播放失败的反馈', 'info');
|
log('初始化Opus解码器...', 'info');
|
||||||
|
try {
|
||||||
|
opusDecoder = await initOpusDecoder();
|
||||||
|
if (!opusDecoder) {
|
||||||
|
throw new Error('解码器初始化失败');
|
||||||
|
}
|
||||||
|
log('Opus解码器初始化成功', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
log('Opus解码器初始化失败: ' + error.message, 'error');
|
||||||
|
isAudioPlaying = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建流式播放上下文
|
||||||
|
if (!streamingContext) {
|
||||||
|
streamingContext = {
|
||||||
|
queue: [], // 已解码的PCM队列
|
||||||
|
playing: false, // 是否正在播放
|
||||||
|
endOfStream: false, // 是否收到结束信号
|
||||||
|
source: null, // 当前音频源
|
||||||
|
totalSamples: 0, // 累积的总样本数
|
||||||
|
lastPlayTime: 0, // 上次播放的时间戳
|
||||||
|
|
||||||
|
// 将Opus数据解码为PCM
|
||||||
|
decodeOpusFrames: async function(opusFrames) {
|
||||||
|
if (!opusDecoder) {
|
||||||
|
log('Opus解码器未初始化,无法解码', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decodedSamples = [];
|
||||||
|
|
||||||
|
for (const frame of opusFrames) {
|
||||||
|
try {
|
||||||
|
// 使用Opus解码器解码
|
||||||
|
const frameData = opusDecoder.decode(frame);
|
||||||
|
if (frameData && frameData.length > 0) {
|
||||||
|
// 转换为Float32
|
||||||
|
const floatData = convertInt16ToFloat32(frameData);
|
||||||
|
decodedSamples.push(...floatData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log("Opus解码失败: " + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodedSamples.length > 0) {
|
||||||
|
// 添加到解码队列
|
||||||
|
this.queue.push(...decodedSamples);
|
||||||
|
this.totalSamples += decodedSamples.length;
|
||||||
|
|
||||||
|
// 如果累积了至少0.2秒的音频,开始播放
|
||||||
|
const minSamples = SAMPLE_RATE * MIN_AUDIO_DURATION;
|
||||||
|
if (!this.playing && this.queue.length >= minSamples) {
|
||||||
|
this.startPlaying();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('没有成功解码的样本', 'warning');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 开始播放音频
|
||||||
|
startPlaying: function() {
|
||||||
|
if (this.playing || this.queue.length === 0) return;
|
||||||
|
|
||||||
|
this.playing = true;
|
||||||
|
|
||||||
|
// 创建新的音频缓冲区
|
||||||
|
const minPlaySamples = Math.min(this.queue.length, SAMPLE_RATE); // 最多播放1秒
|
||||||
|
const currentSamples = this.queue.splice(0, minPlaySamples);
|
||||||
|
|
||||||
|
const audioBuffer = audioContext.createBuffer(CHANNELS, currentSamples.length, SAMPLE_RATE);
|
||||||
|
audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);
|
||||||
|
|
||||||
|
// 创建音频源
|
||||||
|
this.source = audioContext.createBufferSource();
|
||||||
|
this.source.buffer = audioBuffer;
|
||||||
|
|
||||||
|
// 创建增益节点用于平滑过渡
|
||||||
|
const gainNode = audioContext.createGain();
|
||||||
|
|
||||||
|
// 应用淡入淡出效果避免爆音
|
||||||
|
const fadeDuration = 0.02; // 20毫秒
|
||||||
|
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + fadeDuration);
|
||||||
|
|
||||||
|
const duration = audioBuffer.duration;
|
||||||
|
if (duration > fadeDuration * 2) {
|
||||||
|
gainNode.gain.setValueAtTime(1, audioContext.currentTime + duration - fadeDuration);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接节点并开始播放
|
||||||
|
this.source.connect(gainNode);
|
||||||
|
gainNode.connect(audioContext.destination);
|
||||||
|
|
||||||
|
this.lastPlayTime = audioContext.currentTime;
|
||||||
|
log(`开始播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / SAMPLE_RATE).toFixed(2)} 秒`, 'info');
|
||||||
|
|
||||||
|
// 播放结束后的处理
|
||||||
|
this.source.onended = () => {
|
||||||
|
this.source = null;
|
||||||
|
this.playing = false;
|
||||||
|
|
||||||
|
// 如果队列中还有数据或者缓冲区有新数据,继续播放
|
||||||
|
if (this.queue.length > 0) {
|
||||||
|
setTimeout(() => this.startPlaying(), 10);
|
||||||
|
} else if (audioBufferQueue.length > 0) {
|
||||||
|
// 缓冲区有新数据,进行解码
|
||||||
|
const frames = [...audioBufferQueue];
|
||||||
|
audioBufferQueue = [];
|
||||||
|
this.decodeOpusFrames(frames);
|
||||||
|
} else if (this.endOfStream) {
|
||||||
|
// 流已结束且没有更多数据
|
||||||
|
log("音频播放完成", 'info');
|
||||||
|
isAudioPlaying = false;
|
||||||
|
streamingContext = null;
|
||||||
|
} else {
|
||||||
|
// 等待更多数据
|
||||||
|
setTimeout(() => {
|
||||||
|
// 如果仍然没有新数据,但有更多的包到达
|
||||||
|
if (this.queue.length === 0 && audioBufferQueue.length > 0) {
|
||||||
|
const frames = [...audioBufferQueue];
|
||||||
|
audioBufferQueue = [];
|
||||||
|
this.decodeOpusFrames(frames);
|
||||||
|
} else if (this.queue.length === 0 && audioBufferQueue.length === 0) {
|
||||||
|
// 真的没有更多数据了
|
||||||
|
log("音频播放完成 (超时)", 'info');
|
||||||
|
isAudioPlaying = false;
|
||||||
|
streamingContext = null;
|
||||||
|
}
|
||||||
|
}, 500); // 500ms超时
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.source.start();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始处理缓冲的数据
|
||||||
|
const frames = [...audioBufferQueue];
|
||||||
|
audioBufferQueue = []; // 清空缓冲队列
|
||||||
|
|
||||||
|
// 解码并播放
|
||||||
|
await streamingContext.decodeOpusFrames(frames);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log(`播放已缓冲的音频出错: ${error.message}`, 'error');
|
||||||
|
isAudioPlaying = false;
|
||||||
|
streamingContext = null;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行初始化和播放
|
||||||
|
initDecoderAndPlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将Int16音频数据转换为Float32音频数据
|
||||||
|
function convertInt16ToFloat32(int16Data) {
|
||||||
|
const float32Data = new Float32Array(int16Data.length);
|
||||||
|
for (let i = 0; i < int16Data.length; i++) {
|
||||||
|
// 将[-32768,32767]范围转换为[-1,1]
|
||||||
|
float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7FFF);
|
||||||
|
}
|
||||||
|
return float32Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Opus解码器 - 确保完全初始化完成后才返回
|
||||||
|
async function initOpusDecoder() {
|
||||||
|
if (opusDecoder) return opusDecoder; // 已经初始化
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查ModuleInstance是否存在
|
||||||
|
if (typeof window.ModuleInstance === 'undefined') {
|
||||||
|
if (typeof Module !== 'undefined') {
|
||||||
|
// 使用全局Module作为ModuleInstance
|
||||||
|
window.ModuleInstance = Module;
|
||||||
|
log('使用全局Module作为ModuleInstance', 'info');
|
||||||
|
} else {
|
||||||
|
throw new Error('Opus库未加载,ModuleInstance和Module对象都不存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mod = window.ModuleInstance;
|
||||||
|
|
||||||
|
// 创建解码器对象
|
||||||
|
opusDecoder = {
|
||||||
|
channels: CHANNELS,
|
||||||
|
rate: SAMPLE_RATE,
|
||||||
|
frameSize: FRAME_SIZE,
|
||||||
|
module: mod,
|
||||||
|
decoderPtr: null, // 初始为null
|
||||||
|
|
||||||
|
// 初始化解码器
|
||||||
|
init: function() {
|
||||||
|
if (this.decoderPtr) return true; // 已经初始化
|
||||||
|
|
||||||
|
// 获取解码器大小
|
||||||
|
const decoderSize = mod._opus_decoder_get_size(this.channels);
|
||||||
|
log(`Opus解码器大小: ${decoderSize}字节`, 'debug');
|
||||||
|
|
||||||
|
// 分配内存
|
||||||
|
this.decoderPtr = mod._malloc(decoderSize);
|
||||||
|
if (!this.decoderPtr) {
|
||||||
|
throw new Error("无法分配解码器内存");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化解码器
|
||||||
|
const err = mod._opus_decoder_init(
|
||||||
|
this.decoderPtr,
|
||||||
|
this.rate,
|
||||||
|
this.channels
|
||||||
|
);
|
||||||
|
|
||||||
|
if (err < 0) {
|
||||||
|
this.destroy(); // 清理资源
|
||||||
|
throw new Error(`Opus解码器初始化失败: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Opus解码器初始化成功", 'success');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 解码方法
|
||||||
|
decode: function(opusData) {
|
||||||
|
if (!this.decoderPtr) {
|
||||||
|
if (!this.init()) {
|
||||||
|
throw new Error("解码器未初始化且无法初始化");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mod = this.module;
|
||||||
|
|
||||||
|
// 为Opus数据分配内存
|
||||||
|
const opusPtr = mod._malloc(opusData.length);
|
||||||
|
mod.HEAPU8.set(opusData, opusPtr);
|
||||||
|
|
||||||
|
// 为PCM输出分配内存
|
||||||
|
const pcmPtr = mod._malloc(this.frameSize * 2); // Int16 = 2字节
|
||||||
|
|
||||||
|
// 解码
|
||||||
|
const decodedSamples = mod._opus_decode(
|
||||||
|
this.decoderPtr,
|
||||||
|
opusPtr,
|
||||||
|
opusData.length,
|
||||||
|
pcmPtr,
|
||||||
|
this.frameSize,
|
||||||
|
0 // 不使用FEC
|
||||||
|
);
|
||||||
|
|
||||||
|
if (decodedSamples < 0) {
|
||||||
|
mod._free(opusPtr);
|
||||||
|
mod._free(pcmPtr);
|
||||||
|
throw new Error(`Opus解码失败: ${decodedSamples}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制解码后的数据
|
||||||
|
const decodedData = new Int16Array(decodedSamples);
|
||||||
|
for (let i = 0; i < decodedSamples; i++) {
|
||||||
|
decodedData[i] = mod.HEAP16[(pcmPtr >> 1) + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放内存
|
||||||
|
mod._free(opusPtr);
|
||||||
|
mod._free(pcmPtr);
|
||||||
|
|
||||||
|
return decodedData;
|
||||||
|
} catch (error) {
|
||||||
|
log(`Opus解码错误: ${error.message}`, 'error');
|
||||||
|
return new Int16Array(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 销毁方法
|
||||||
|
destroy: function() {
|
||||||
|
if (this.decoderPtr) {
|
||||||
|
this.module._free(this.decoderPtr);
|
||||||
|
this.decoderPtr = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化解码器
|
||||||
|
if (!opusDecoder.init()) {
|
||||||
|
throw new Error("Opus解码器初始化失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return opusDecoder;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`简单播放回退方案也失败: ${error.message}`, 'error');
|
log(`Opus解码器初始化失败: ${error.message}`, 'error');
|
||||||
|
opusDecoder = null; // 重置为null,以便下次重试
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1481,6 +1630,14 @@
|
|||||||
|
|
||||||
// 初始化Opus编码器
|
// 初始化Opus编码器
|
||||||
initOpusEncoder();
|
initOpusEncoder();
|
||||||
|
|
||||||
|
// 预加载Opus解码器
|
||||||
|
log('预加载Opus解码器...', 'info');
|
||||||
|
initOpusDecoder().then(() => {
|
||||||
|
log('Opus解码器预加载成功', 'success');
|
||||||
|
}).catch(error => {
|
||||||
|
log(`Opus解码器预加载失败: ${error.message},将在需要时重试`, 'warning');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// PCM录音处理器代码 - 会被注入到AudioWorklet中
|
// PCM录音处理器代码 - 会被注入到AudioWorklet中
|
||||||
@@ -1647,7 +1804,6 @@
|
|||||||
let audioProcessor = null;
|
let audioProcessor = null;
|
||||||
let audioProcessorType = null;
|
let audioProcessorType = null;
|
||||||
let audioSource = null;
|
let audioSource = null;
|
||||||
let pcmBuffers = [];
|
|
||||||
|
|
||||||
// 处理PCM缓冲数据
|
// 处理PCM缓冲数据
|
||||||
let pcmDataBuffer = new Int16Array();
|
let pcmDataBuffer = new Int16Array();
|
||||||
@@ -1895,7 +2051,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理二进制消息
|
|
||||||
async function handleBinaryMessage(data) {
|
async function handleBinaryMessage(data) {
|
||||||
try {
|
try {
|
||||||
let arrayBuffer;
|
let arrayBuffer;
|
||||||
@@ -1917,10 +2072,25 @@
|
|||||||
const opusData = new Uint8Array(arrayBuffer);
|
const opusData = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
if (opusData.length > 0) {
|
if (opusData.length > 0) {
|
||||||
// 播放Opus音频
|
// 将数据添加到缓冲队列
|
||||||
playOpusData([opusData]);
|
audioBufferQueue.push(opusData);
|
||||||
|
|
||||||
|
// 如果收到的是第一个音频包,开始缓冲过程
|
||||||
|
if (audioBufferQueue.length === 1 && !isAudioBuffering && !isAudioPlaying) {
|
||||||
|
startAudioBuffering();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log('收到空音频数据帧', 'warning');
|
log('收到空音频数据帧,可能是结束标志', 'warning');
|
||||||
|
|
||||||
|
// 如果缓冲队列中有数据且没有在播放,立即开始播放
|
||||||
|
if (audioBufferQueue.length > 0 && !isAudioPlaying) {
|
||||||
|
playBufferedAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果正在播放,发送结束信号
|
||||||
|
if (isAudioPlaying && streamingContext) {
|
||||||
|
streamingContext.endOfStream = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`处理二进制消息出错: ${error.message}`, 'error');
|
log(`处理二进制消息出错: ${error.message}`, 'error');
|
||||||
|
|||||||
Reference in New Issue
Block a user