update:修复测试工具页面声音卡顿问题

This commit is contained in:
hrz
2025-11-15 17:53:36 +08:00
parent b55e5870db
commit dc55f8d7ab
2 changed files with 41 additions and 33 deletions
+33 -25
View File
@@ -48,8 +48,8 @@ export class StreamingContext {
convertInt16ToFloat32(int16Data) { convertInt16ToFloat32(int16Data) {
const float32Data = new Float32Array(int16Data.length); const float32Data = new Float32Array(int16Data.length);
for (let i = 0; i < int16Data.length; i++) { for (let i = 0; i < int16Data.length; i++) {
// 将[-32768,32767]范围转换为[-1,1] // 将[-32768,32767]范围转换为[-1,1],统一使用32768.0避免不对称失真
float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7FFF); float32Data[i] = int16Data[i] / 32768.0;
} }
return float32Data; return float32Data;
} }
@@ -97,18 +97,26 @@ export class StreamingContext {
// 开始播放音频 // 开始播放音频
async startPlaying() { async startPlaying() {
let scheduledEndTime = this.audioContext.currentTime; // 跟踪已调度音频的结束时间
while (true) { 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) { if (!this.playing && this.queue.length < minSamples) {
await this.getQueue(minSamples); await this.getQueue(minSamples);
} }
this.playing = true; 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); const audioBuffer = this.audioContext.createBuffer(this.channels, currentSamples.length, this.sampleRate);
audioBuffer.copyToChannel(new Float32Array(currentSamples), 0); audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);
@@ -116,28 +124,28 @@ export class StreamingContext {
this.source = this.audioContext.createBufferSource(); this.source = this.audioContext.createBufferSource();
this.source.buffer = audioBuffer; 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毫秒 this.source.connect(this.audioContext.destination);
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(1, this.audioContext.currentTime + fadeDuration);
log(`调度播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / this.sampleRate).toFixed(2)}`, 'debug');
this.source.start(startTime);
// 更新下一个音频块的调度时间
const duration = audioBuffer.duration; const duration = audioBuffer.duration;
if (duration > fadeDuration * 2) { scheduledEndTime = startTime + duration;
gainNode.gain.setValueAtTime(1, this.audioContext.currentTime + duration - fadeDuration); this.lastPlayTime = startTime;
gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + duration);
// 如果队列中数据不足,等待新数据
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); await this.getQueue(minSamples);
} }
} }
+8 -8
View File
@@ -445,8 +445,8 @@
let audioBufferQueue = []; // 存储接收到的音频包 let audioBufferQueue = []; // 存储接收到的音频包
let isAudioPlaying = false; // 是否正在播放音频 let isAudioPlaying = false; // 是否正在播放音频
const BUFFER_THRESHOLD = 3; // 缓冲包数量阈值,至少累积3个包再开始播放 const BUFFER_THRESHOLD = 6; // 缓冲包数量阈值,6个包(360ms)平衡延迟和流畅度
const MIN_AUDIO_DURATION = 0.1; // 最小音频长度(秒)小于这个长度的音频会被合并 const MIN_AUDIO_DURATION = 0.12; // 最小音频长度(秒)对应2个Opus包(120ms)
let streamingContext = null; // 音频流上下文 let streamingContext = null; // 音频流上下文
const SAMPLE_RATE = 16000; // 采样率 const SAMPLE_RATE = 16000; // 采样率
const CHANNELS = 1; // 声道数 const CHANNELS = 1; // 声道数
@@ -518,12 +518,12 @@
log(`预初始化Opus解码器失败: ${error.message}`, 'warning'); log(`预初始化Opus解码器失败: ${error.message}`, 'warning');
// 继续缓冲,我们会在播放时再次尝试初始化 // 继续缓冲,我们会在播放时再次尝试初始化
}); });
const timeout = 300; const timeout = 400;
while (true) { while (true) {
// 每次数据空的时候等三条数据 // 初始缓冲:等待6个包,平衡延迟和流畅度
const packets = await queue.dequeue( const packets = await queue.dequeue(
3, // 至少 3 6, // 至少 6360ms缓冲
timeout, // 最多等 300 ms timeout, // 最多等 400 ms
(count) => { // 超时额外回调 (count) => { // 超时额外回调
log(`缓冲超时,当前缓冲包数: ${count},开始播放`, 'info'); log(`缓冲超时,当前缓冲包数: ${count},开始播放`, 'info');
} }
@@ -532,9 +532,9 @@
log(`已缓冲 ${packets.length} 个音频包,开始播放`, 'info'); log(`已缓冲 ${packets.length} 个音频包,开始播放`, 'info');
streamingContext.pushAudioBuffer(packets) streamingContext.pushAudioBuffer(packets)
} }
// 50毫秒里,有多少给多少 // 持续缓冲:快速获取数据,每30ms检查一次
while (true) { while (true) {
const data = await queue.dequeue(99, 50) const data = await queue.dequeue(99, 30)
if (data.length) { if (data.length) {
streamingContext.pushAudioBuffer(data) streamingContext.pushAudioBuffer(data)
} else { } else {