mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-31 03:43:57 +08:00
优化:优化播放逻辑去除多个方法共享属性导致的逻辑错误
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user