From dc55f8d7ab34bc36318cd42929d58d89274f8cdb Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Sat, 15 Nov 2025 17:53:36 +0800 Subject: [PATCH] =?UTF-8?q?update:=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=A1=B5=E9=9D=A2=E5=A3=B0=E9=9F=B3=E5=8D=A1?= =?UTF-8?q?=E9=A1=BF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/js/StreamingContext.js | 58 +++++++++++-------- main/xiaozhi-server/test/test_page.html | 16 ++--- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/main/xiaozhi-server/test/js/StreamingContext.js b/main/xiaozhi-server/test/js/StreamingContext.js index 576d9761..761c0691 100644 --- a/main/xiaozhi-server/test/js/StreamingContext.js +++ b/main/xiaozhi-server/test/js/StreamingContext.js @@ -48,8 +48,8 @@ export class StreamingContext { 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); + // 将[-32768,32767]范围转换为[-1,1],统一使用32768.0避免不对称失真 + float32Data[i] = int16Data[i] / 32768.0; } return float32Data; } @@ -97,18 +97,26 @@ export class StreamingContext { // 开始播放音频 async startPlaying() { + let scheduledEndTime = this.audioContext.currentTime; // 跟踪已调度音频的结束时间 + while (true) { - // 如果累积了至少0.3秒的音频,开始播放 - const minSamples = this.sampleRate * this.minAudioDuration * 3; + // 初始缓冲:等待足够的样本再开始播放 + const minSamples = this.sampleRate * this.minAudioDuration * 2; if (!this.playing && this.queue.length < minSamples) { await this.getQueue(minSamples); } this.playing = true; - while (this.playing && this.queue.length) { - // 创建新的音频缓冲区 - const minPlaySamples = Math.min(this.queue.length, this.sampleRate); - const currentSamples = this.queue.splice(0, minPlaySamples); + // 持续播放队列中的音频,每次播放一个小块 + while (this.playing && this.queue.length > 0) { + // 每次播放120ms的音频(2个Opus包) + const playDuration = 0.12; + const targetSamples = Math.floor(this.sampleRate * playDuration); + const actualSamples = Math.min(this.queue.length, targetSamples); + + if (actualSamples === 0) break; + + const currentSamples = this.queue.splice(0, actualSamples); const audioBuffer = this.audioContext.createBuffer(this.channels, currentSamples.length, this.sampleRate); audioBuffer.copyToChannel(new Float32Array(currentSamples), 0); @@ -116,28 +124,28 @@ export class StreamingContext { this.source = this.audioContext.createBufferSource(); this.source.buffer = audioBuffer; - // 创建增益节点用于平滑过渡 - const gainNode = this.audioContext.createGain(); + // 精确调度播放时间 + const currentTime = this.audioContext.currentTime; + const startTime = Math.max(scheduledEndTime, currentTime); - // 应用淡入淡出效果避免爆音 - const fadeDuration = 0.02; // 20毫秒 - gainNode.gain.setValueAtTime(0, this.audioContext.currentTime); - gainNode.gain.linearRampToValueAtTime(1, this.audioContext.currentTime + fadeDuration); + // 直接连接到输出 + this.source.connect(this.audioContext.destination); + log(`调度播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / this.sampleRate).toFixed(2)} 秒`, 'debug'); + this.source.start(startTime); + + // 更新下一个音频块的调度时间 const duration = audioBuffer.duration; - if (duration > fadeDuration * 2) { - gainNode.gain.setValueAtTime(1, this.audioContext.currentTime + duration - fadeDuration); - gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + duration); + scheduledEndTime = startTime + duration; + this.lastPlayTime = startTime; + + // 如果队列中数据不足,等待新数据 + if (this.queue.length < targetSamples) { + break; } - - // 连接节点并开始播放 - this.source.connect(gainNode); - gainNode.connect(this.audioContext.destination); - - this.lastPlayTime = this.audioContext.currentTime; - log(`开始播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / this.sampleRate).toFixed(2)} 秒`, 'info'); - this.source.start(); } + + // 等待新数据 await this.getQueue(minSamples); } } diff --git a/main/xiaozhi-server/test/test_page.html b/main/xiaozhi-server/test/test_page.html index b9fc5c85..88f182c4 100644 --- a/main/xiaozhi-server/test/test_page.html +++ b/main/xiaozhi-server/test/test_page.html @@ -445,8 +445,8 @@ let audioBufferQueue = []; // 存储接收到的音频包 let isAudioPlaying = false; // 是否正在播放音频 - const BUFFER_THRESHOLD = 3; // 缓冲包数量阈值,至少累积3个包再开始播放 - const MIN_AUDIO_DURATION = 0.1; // 最小音频长度(秒),小于这个长度的音频会被合并 + const BUFFER_THRESHOLD = 6; // 缓冲包数量阈值,6个包(360ms)平衡延迟和流畅度 + const MIN_AUDIO_DURATION = 0.12; // 最小音频长度(秒),对应2个Opus包(120ms) let streamingContext = null; // 音频流上下文 const SAMPLE_RATE = 16000; // 采样率 const CHANNELS = 1; // 声道数 @@ -518,12 +518,12 @@ log(`预初始化Opus解码器失败: ${error.message}`, 'warning'); // 继续缓冲,我们会在播放时再次尝试初始化 }); - const timeout = 300; + const timeout = 400; while (true) { - // 每次数据空的时候等三条数据 + // 初始缓冲:等待6个包,平衡延迟和流畅度 const packets = await queue.dequeue( - 3, // 至少 3 条 - timeout, // 最多等 300 ms + 6, // 至少 6 条,360ms缓冲 + timeout, // 最多等 400 ms (count) => { // 超时额外回调 log(`缓冲超时,当前缓冲包数: ${count},开始播放`, 'info'); } @@ -532,9 +532,9 @@ log(`已缓冲 ${packets.length} 个音频包,开始播放`, 'info'); streamingContext.pushAudioBuffer(packets) } - // 50毫秒里,有多少给多少 + // 持续缓冲:快速获取数据,每30ms检查一次 while (true) { - const data = await queue.dequeue(99, 50) + const data = await queue.dequeue(99, 30) if (data.length) { streamingContext.pushAudioBuffer(data) } else {