优化:优化播放逻辑去除多个方法共享属性导致的逻辑错误

This commit is contained in:
JianYu Zheng
2025-08-15 11:26:14 +08:00
parent 7c0df908e9
commit 4423cb85c2
2 changed files with 1525 additions and 1454 deletions
@@ -7,9 +7,16 @@ export default class BlockingQueue {
#emptyResolve = null; #emptyResolve = null;
/* 生产者:把数据塞进去 */ /* 生产者:把数据塞进去 */
enqueue(item) { enqueue(item, ...restItems) {
if (restItems.length === 0) {
this.#items.push(item); this.#items.push(item);
}
// 如果有额外参数,批量处理所有项
else {
const items = [item, ...restItems].filter(i => i);
if (items.length === 0) return;
this.#items.push(...items);
}
// 若有空队列闸门,一次性放行所有等待者 // 若有空队列闸门,一次性放行所有等待者
if (this.#emptyResolve) { if (this.#emptyResolve) {
this.#emptyResolve(); this.#emptyResolve();
+154 -90
View File
@@ -12,7 +12,8 @@
<h1>小智服务器测试页面</h1> <h1>小智服务器测试页面</h1>
<div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);"> <div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
正在加载Opus库...</div> 正在加载Opus库...
</div>
<!-- 添加配置面板 --> <!-- 添加配置面板 -->
<div class="section"> <div class="section">
@@ -189,7 +190,8 @@
} }
const queue = new BlockingQueue(); const queue = new BlockingQueue();
// 开始音频缓冲过程
// 启动缓存进程
async function startAudioBuffering() { async function startAudioBuffering() {
log("开始音频缓冲...", 'info'); log("开始音频缓冲...", 'info');
@@ -200,6 +202,7 @@
}); });
const timeout = 300; const timeout = 300;
while (true) { while (true) {
// 每次数据空的时候等三条数据
const packets = await queue.dequeue( const packets = await queue.dequeue(
3, // 至少 3 条 3, // 至少 3 条
timeout, // 最多等 300 ms timeout, // 最多等 300 ms
@@ -209,28 +212,23 @@
); );
if (packets.length) { if (packets.length) {
log(`已缓冲 ${packets.length} 个音频包,开始播放`, 'info'); log(`已缓冲 ${packets.length} 个音频包,开始播放`, 'info');
audioBufferQueue.push(...packets) streamingContext.pushAudioBuffer(packets)
playBufferedAudio();
} }
console.log(queue.length) // 50毫秒里,有多少给多少
while (true) { while (true) {
const data = await queue.dequeue(99, 50) const data = await queue.dequeue(99, 50)
if (data.length) { if (data.length) {
audioBufferQueue.push(...data) streamingContext.pushAudioBuffer(data)
playBufferedAudio(); } else {
break
} }
} }
} }
} }
// 播放已缓冲的音频 // 播放已缓冲的音频
function playBufferedAudio() { async function playBufferedAudio() {
if (isAudioPlaying || audioBufferQueue.length === 0) return;
isAudioPlaying = true;
// 确保Opus解码器已初始化 // 确保Opus解码器已初始化
const initDecoderAndPlay = async () => {
try { try {
// 确保音频上下文存在 // 确保音频上下文存在
if (!audioContext) { if (!audioContext) {
@@ -259,24 +257,48 @@
// 创建流式播放上下文 // 创建流式播放上下文
if (!streamingContext) { if (!streamingContext) {
streamingContext = { streamingContext = {
queue: [], // 已解码的PCM队列 queue: [], // 已解码的PCM队列。正在播放
activeQueue: new BlockingQueue(), // 已解码的PCM队列。准备播放
pendingAudioBufferQueue: [], // 待处理的缓存队列
audioBufferQueue: new BlockingQueue(), // 缓存队列
playing: false, // 是否正在播放 playing: false, // 是否正在播放
endOfStream: false, // 是否收到结束信号 endOfStream: false, // 是否收到结束信号
source: null, // 当前音频源 source: null, // 当前音频源
totalSamples: 0, // 累积的总样本数 totalSamples: 0, // 累积的总样本数
lastPlayTime: 0, // 上次播放的时间戳 lastPlayTime: 0, // 上次播放的时间戳
i: 1,
// 缓存音频数组
pushAudioBuffer: function (item) {
this.audioBufferQueue.enqueue(...item)
},
// 获取需要处理缓存队列,单线程:在audioBufferQueue一直更新的状态下不会出现安全问题
getPendingAudioBufferQueue: async function () {
// 原子交换 + 清空
[this.pendingAudioBufferQueue, this.audioBufferQueue] = [await this.audioBufferQueue.dequeue(), new BlockingQueue()];
},
// 获取正在播放已解码的PCM队列,单线程:在activeQueue一直更新的状态下不会出现安全问题
getQueue: async function (minSamples) {
let TepArray = []
const num = minSamples - this.queue.length > 0 ? minSamples -this.queue.length : 1;
// 原子交换 + 清空
[TepArray, this.activeQueue] = [await this.activeQueue.dequeue(num), new BlockingQueue()];
this.queue.push(...TepArray)
},
// 将Opus数据解码为PCM // 将Opus数据解码为PCM
decodeOpusFrames: async function (opusFrames) { decodeOpusFrames: async function () {
if (!opusDecoder) { if (!opusDecoder) {
log('Opus解码器未初始化,无法解码', 'error'); log('Opus解码器未初始化,无法解码', 'error');
return; return;
} else {
log('Opus解码器启动', 'info');
} }
while (true) {
let decodedSamples = []; let decodedSamples = [];
for (const frame of this.pendingAudioBufferQueue) {
for (const frame of opusFrames) {
try { try {
// 使用Opus解码器解码 // 使用Opus解码器解码
const frameData = opusDecoder.decode(frame); const frameData = opusDecoder.decode(frame);
@@ -296,26 +318,26 @@
if (decodedSamples.length > 0) { if (decodedSamples.length > 0) {
// 使用循环替代展开运算符 // 使用循环替代展开运算符
for (let i = 0; i < decodedSamples.length; i++) { for (let i = 0; i < decodedSamples.length; i++) {
this.queue.push(decodedSamples[i]); this.activeQueue.enqueue(decodedSamples[i]);
} }
this.totalSamples += decodedSamples.length; this.totalSamples += decodedSamples.length;
// 如果累积了至少0.2秒的音频,开始播放
const minSamples = SAMPLE_RATE * MIN_AUDIO_DURATION;
if (!this.playing && this.queue.length >= minSamples) {
this.startPlaying();
}
} else { } else {
log('没有成功解码的样本', 'warning'); log('没有成功解码的样本', 'warning');
} }
await this.getPendingAudioBufferQueue();
}
}, },
// 开始播放音频 // 开始播放音频
startPlaying: function () { startPlaying: async function () {
if (this.playing || this.queue.length === 0) return; while (true) {
// 如果累积了至少0.2秒的音频,开始播放
const minSamples = SAMPLE_RATE * MIN_AUDIO_DURATION;
if (!this.playing && this.queue.length < minSamples) {
await this.getQueue(minSamples)
}
this.playing = true; this.playing = true;
while (this.playing && this.queue.length) {
// 创建新的音频缓冲区 // 创建新的音频缓冲区
const minPlaySamples = Math.min(this.queue.length, SAMPLE_RATE); const minPlaySamples = Math.min(this.queue.length, SAMPLE_RATE);
const currentSamples = this.queue.splice(0, minPlaySamples); const currentSamples = this.queue.splice(0, minPlaySamples);
@@ -347,68 +369,61 @@
this.lastPlayTime = audioContext.currentTime; this.lastPlayTime = audioContext.currentTime;
log(`开始播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / SAMPLE_RATE).toFixed(2)}`, 'info'); log(`开始播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / SAMPLE_RATE).toFixed(2)}`, 'info');
// 播放结束后的处理
this.source.onended = () => {
this.source = null;
this.playing = false;
// 使用setTimeout避免递归调用
setTimeout(() => {
// 如果队列中还有数据,继续播放
if (this.queue.length > 0) {
this.startPlaying();
} else if (audioBufferQueue.length > 0) {
// 缓冲区有新数据,进行解码
const frames = [...audioBufferQueue];
audioBufferQueue = [];
this.decodeOpusFrames(frames);
} else if (this.endOfStream) {
// 流已结束且没有更多数据
log("音频播放完成", 'info');
isAudioPlaying = false;
this.endOfStream = 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超时
}
}, 10); // 10ms延迟,避免立即递归
};
this.source.start(); this.source.start();
} }
await this.getQueue(minSamples)
}
// // 播放结束后的处理
// this.source.onended = () => {
// this.source = null;
// this.playing = false;
//
// // 使用setTimeout避免递归调用
// setTimeout(() => {
// // 如果队列中还有数据,继续播放
// if (this.queue.length > 0) {
// this.startPlaying();
// } else if (audioBufferQueue.length > 0) {
// // 缓冲区有新数据,进行解码
// const frames = [...audioBufferQueue];
// audioBufferQueue = [];
// this.decodeOpusFrames(frames);
// } else if (this.endOfStream) {
// // 流已结束且没有更多数据
// log("音频播放完成", 'info');
// isAudioPlaying = false;
// this.endOfStream = 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超时
// }
// }, 10); // 10ms延迟,避免立即递归
// };
}
}; };
} }
// 开始处理缓冲的数据 streamingContext.decodeOpusFrames();
const frames = [...audioBufferQueue]; streamingContext.startPlaying();
audioBufferQueue = []; // 清空缓冲队列
// 解码并播放
await streamingContext.decodeOpusFrames(frames);
} catch (error) { } catch (error) {
log(`播放已缓冲的音频出错: ${error.message}`, 'error'); log(`播放已缓冲的音频出错: ${error.message}`, 'error');
isAudioPlaying = false; isAudioPlaying = false;
streamingContext = null; streamingContext = null;
} }
};
// 执行初始化和播放
initDecoderAndPlay();
} }
// 将Int16音频数据转换为Float32音频数据 // 将Int16音频数据转换为Float32音频数据
@@ -887,12 +902,66 @@
if (payload) { if (payload) {
// 模拟小智客户端行为 // 模拟小智客户端行为
if (payload.method === 'tools/list') { if (payload.method === 'tools/list') {
const replay_message = JSON.stringify({"session_id":"","type":"mcp","payload":{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"self.get_device_status","description":"Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\nUse this tool for: \n1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)","inputSchema":{"type":"object","properties":{}}},{"name":"self.audio_speaker.set_volume","description":"Set the volume of the audio speaker. If the current volume is unknown, you must call `self.get_device_status` tool first and then call this tool.","inputSchema":{"type":"object","properties":{"volume":{"type":"integer","minimum":0,"maximum":100}},"required":["volume"]}},{"name":"self.screen.set_brightness","description":"Set the brightness of the screen.","inputSchema":{"type":"object","properties":{"brightness":{"type":"integer","minimum":0,"maximum":100}},"required":["brightness"]}},{"name":"self.screen.set_theme","description":"Set the theme of the screen. The theme can be 'light' or 'dark'.","inputSchema":{"type":"object","properties":{"theme":{"type":"string"}},"required":["theme"]}}]}}}) const replay_message = JSON.stringify({
"session_id": "", "type": "mcp", "payload": {
"jsonrpc": "2.0", "id": 2, "result": {
"tools": [{
"name": "self.get_device_status",
"description": "Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\nUse this tool for: \n1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)",
"inputSchema": {"type": "object", "properties": {}}
}, {
"name": "self.audio_speaker.set_volume",
"description": "Set the volume of the audio speaker. If the current volume is unknown, you must call `self.get_device_status` tool first and then call this tool.",
"inputSchema": {
"type": "object",
"properties": {
"volume": {
"type": "integer",
"minimum": 0,
"maximum": 100
}
},
"required": ["volume"]
}
}, {
"name": "self.screen.set_brightness",
"description": "Set the brightness of the screen.",
"inputSchema": {
"type": "object",
"properties": {
"brightness": {
"type": "integer",
"minimum": 0,
"maximum": 100
}
},
"required": ["brightness"]
}
}, {
"name": "self.screen.set_theme",
"description": "Set the theme of the screen. The theme can be 'light' or 'dark'.",
"inputSchema": {
"type": "object",
"properties": {"theme": {"type": "string"}},
"required": ["theme"]
}
}]
}
}
})
websocket.send(replay_message); websocket.send(replay_message);
log(`回复MCP消息: ${replay_message}`, 'info'); log(`回复MCP消息: ${replay_message}`, 'info');
} else if (payload.method === 'tools/call') { } else if (payload.method === 'tools/call') {
// 模拟回复 // 模拟回复
const replay_message = JSON.stringify({"session_id":"9f261599","type":"mcp","payload":{"jsonrpc":"2.0","id": payload.id,"result":{"content":[{"type":"text","text":"true"}],"isError":false}}}) const replay_message = JSON.stringify({
"session_id": "9f261599",
"type": "mcp",
"payload": {
"jsonrpc": "2.0",
"id": payload.id,
"result": {"content": [{"type": "text", "text": "true"}], "isError": false}
}
})
websocket.send(replay_message); websocket.send(replay_message);
log(`回复MCP消息: ${replay_message}`, 'info'); log(`回复MCP消息: ${replay_message}`, 'info');
} }
@@ -991,8 +1060,6 @@
const message = messageInput.value.trim(); const message = messageInput.value.trim();
if (message === '' || !websocket || websocket.readyState !== WebSocket.OPEN) return; if (message === '' || !websocket || websocket.readyState !== WebSocket.OPEN) return;
audioBufferQueue = [];
isAudioPlaying = false;
try { try {
// 直接发送listen消息,不需要重复发送hello // 直接发送listen消息,不需要重复发送hello
@@ -1244,7 +1311,9 @@
}).catch(error => { }).catch(error => {
log(`Opus解码器预加载失败: ${error.message},将在需要时重试`, 'warning'); log(`Opus解码器预加载失败: ${error.message},将在需要时重试`, 'warning');
}); });
playBufferedAudio()
startAudioBuffering() startAudioBuffering()
} }
// PCM录音处理器代码 - 会被注入到AudioWorklet中 // PCM录音处理器代码 - 会被注入到AudioWorklet中
@@ -1414,6 +1483,7 @@
// 处理PCM缓冲数据 // 处理PCM缓冲数据
let pcmDataBuffer = new Int16Array(); let pcmDataBuffer = new Int16Array();
function processPCMBuffer(buffer) { function processPCMBuffer(buffer) {
if (!isRecording) return; if (!isRecording) return;
@@ -1680,12 +1750,6 @@
queue.enqueue(opusData); queue.enqueue(opusData);
} else { } else {
log('收到空音频数据帧,可能是结束标志', 'warning'); log('收到空音频数据帧,可能是结束标志', 'warning');
// 如果缓冲队列中有数据且没有在播放,立即开始播放
if (audioBufferQueue.length > 0 && !isAudioPlaying) {
playBufferedAudio();
}
// 如果正在播放,发送结束信号 // 如果正在播放,发送结束信号
if (isAudioPlaying && streamingContext) { if (isAudioPlaying && streamingContext) {
streamingContext.endOfStream = true; streamingContext.endOfStream = true;