From f50521e54892d99ea2843d0007197c04bf455868 Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Wed, 5 Nov 2025 18:05:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BF=E7=94=9F=E4=BA=BAAI=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E6=B5=8B=E8=AF=95=E7=BD=91=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bot_web_test/abbreviated_version/app.js | 1254 ++++++++++ .../abbreviated_version/test.html | 503 ++++ vue/apps/bot_web_test/default-mcp-tools.json | 72 + vue/apps/bot_web_test/js/StreamingContext.js | 149 ++ vue/apps/bot_web_test/js/document.js | 49 + vue/apps/bot_web_test/js/opus.js | 186 ++ .../bot_web_test/js/utils/BlockingQueue.js | 98 + vue/apps/bot_web_test/js/utils/logger.js | 37 + vue/apps/bot_web_test/js/xiaoZhiConnect.js | 124 + vue/apps/bot_web_test/libopus.js | 266 ++ vue/apps/bot_web_test/opus_test/app.js | 670 +++++ vue/apps/bot_web_test/opus_test/test.html | 464 ++++ vue/apps/bot_web_test/test_page.css | 425 ++++ vue/apps/bot_web_test/test_page.html | 2224 +++++++++++++++++ 14 files changed, 6521 insertions(+) create mode 100644 vue/apps/bot_web_test/abbreviated_version/app.js create mode 100644 vue/apps/bot_web_test/abbreviated_version/test.html create mode 100644 vue/apps/bot_web_test/default-mcp-tools.json create mode 100644 vue/apps/bot_web_test/js/StreamingContext.js create mode 100644 vue/apps/bot_web_test/js/document.js create mode 100644 vue/apps/bot_web_test/js/opus.js create mode 100644 vue/apps/bot_web_test/js/utils/BlockingQueue.js create mode 100644 vue/apps/bot_web_test/js/utils/logger.js create mode 100644 vue/apps/bot_web_test/js/xiaoZhiConnect.js create mode 100644 vue/apps/bot_web_test/libopus.js create mode 100644 vue/apps/bot_web_test/opus_test/app.js create mode 100644 vue/apps/bot_web_test/opus_test/test.html create mode 100644 vue/apps/bot_web_test/test_page.css create mode 100644 vue/apps/bot_web_test/test_page.html diff --git a/vue/apps/bot_web_test/abbreviated_version/app.js b/vue/apps/bot_web_test/abbreviated_version/app.js new file mode 100644 index 0000000..b228652 --- /dev/null +++ b/vue/apps/bot_web_test/abbreviated_version/app.js @@ -0,0 +1,1254 @@ +const SAMPLE_RATE = 16000; +const CHANNELS = 1; +const FRAME_SIZE = 960; // 对应于60ms帧大小 (16000Hz * 0.06s = 960 samples) +const OPUS_APPLICATION = 2049; // OPUS_APPLICATION_AUDIO +const BUFFER_SIZE = 4096; + +// WebSocket相关变量 +let websocket = null; +let isConnected = false; + +let audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE }); +let mediaStream, mediaSource, audioProcessor; +let recordedPcmData = []; // 存储原始PCM数据 +let recordedOpusData = []; // 存储Opus编码后的数据 +let opusEncoder, opusDecoder; +let isRecording = false; + +const startButton = document.getElementById("start"); +const stopButton = document.getElementById("stop"); +const playButton = document.getElementById("play"); +const statusLabel = document.getElementById("status"); + +// 添加WebSocket界面元素引用 +const connectButton = document.getElementById("connectButton") || document.createElement("button"); +const serverUrlInput = document.getElementById("serverUrl") || document.createElement("input"); +const connectionStatus = document.getElementById("connectionStatus") || document.createElement("span"); +const sendTextButton = document.getElementById("sendTextButton") || document.createElement("button"); +const messageInput = document.getElementById("messageInput") || document.createElement("input"); +const conversationDiv = document.getElementById("conversation") || document.createElement("div"); + +// 添加连接和发送事件监听 +if(connectButton.id === "connectButton") { + connectButton.addEventListener("click", connectToServer); +} +if(sendTextButton.id === "sendTextButton") { + sendTextButton.addEventListener("click", sendTextMessage); +} + +startButton.addEventListener("click", startRecording); +stopButton.addEventListener("click", stopRecording); +playButton.addEventListener("click", playRecording); + +// 音频缓冲和播放管理 +let audioBufferQueue = []; // 存储接收到的音频包 +let isAudioBuffering = false; // 是否正在缓冲音频 +let isAudioPlaying = false; // 是否正在播放音频 +const BUFFER_THRESHOLD = 3; // 缓冲包数量阈值,至少累积5个包再开始播放 +const MIN_AUDIO_DURATION = 0.1; // 最小音频长度(秒),小于这个长度的音频会被合并 +let streamingContext = null; // 音频流上下文 + +// 初始化Opus编码器与解码器 +async function initOpus() { + if (typeof window.ModuleInstance === 'undefined') { + if (typeof Module !== 'undefined') { + // 尝试使用全局Module + window.ModuleInstance = Module; + console.log('使用全局Module作为ModuleInstance'); + } else { + console.error("Opus库未加载,ModuleInstance和Module对象都不存在"); + return false; + } + } + + try { + const mod = window.ModuleInstance; + + // 创建编码器 + opusEncoder = { + channels: CHANNELS, + sampleRate: SAMPLE_RATE, + frameSize: FRAME_SIZE, + maxPacketSize: 4000, + module: mod, + + // 初始化编码器 + init: function() { + // 获取编码器大小 + const encoderSize = mod._opus_encoder_get_size(this.channels); + console.log(`Opus编码器大小: ${encoderSize}字节`); + + // 分配内存 + this.encoderPtr = mod._malloc(encoderSize); + if (!this.encoderPtr) { + throw new Error("无法分配编码器内存"); + } + + // 初始化编码器 + const err = mod._opus_encoder_init( + this.encoderPtr, + this.sampleRate, + this.channels, + OPUS_APPLICATION + ); + + if (err < 0) { + throw new Error(`Opus编码器初始化失败: ${err}`); + } + + return true; + }, + + // 编码方法 + encode: function(pcmData) { + const mod = this.module; + + // 为PCM数据分配内存 + const pcmPtr = mod._malloc(pcmData.length * 2); // Int16 = 2字节 + + // 将数据复制到WASM内存 + for (let i = 0; i < pcmData.length; i++) { + mod.HEAP16[(pcmPtr >> 1) + i] = pcmData[i]; + } + + // 为Opus编码数据分配内存 + const maxEncodedSize = this.maxPacketSize; + const encodedPtr = mod._malloc(maxEncodedSize); + + // 编码 + const encodedBytes = mod._opus_encode( + this.encoderPtr, + pcmPtr, + this.frameSize, + encodedPtr, + maxEncodedSize + ); + + if (encodedBytes < 0) { + mod._free(pcmPtr); + mod._free(encodedPtr); + throw new Error(`Opus编码失败: ${encodedBytes}`); + } + + // 复制编码后的数据 + const encodedData = new Uint8Array(encodedBytes); + for (let i = 0; i < encodedBytes; i++) { + encodedData[i] = mod.HEAPU8[encodedPtr + i]; + } + + // 释放内存 + mod._free(pcmPtr); + mod._free(encodedPtr); + + return encodedData; + }, + + // 销毁方法 + destroy: function() { + if (this.encoderPtr) { + this.module._free(this.encoderPtr); + this.encoderPtr = null; + } + } + }; + + // 创建解码器 + opusDecoder = { + channels: CHANNELS, + rate: SAMPLE_RATE, + frameSize: FRAME_SIZE, + module: mod, + + // 初始化解码器 + init: function() { + // 获取解码器大小 + const decoderSize = mod._opus_decoder_get_size(this.channels); + console.log(`Opus解码器大小: ${decoderSize}字节`); + + // 分配内存 + 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) { + throw new Error(`Opus解码器初始化失败: ${err}`); + } + + return true; + }, + + // 解码方法 + decode: function(opusData) { + 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; + }, + + // 销毁方法 + destroy: function() { + if (this.decoderPtr) { + this.module._free(this.decoderPtr); + this.decoderPtr = null; + } + } + }; + + // 初始化编码器和解码器 + if (opusEncoder.init() && opusDecoder.init()) { + console.log("Opus 编码器和解码器初始化成功。"); + return true; + } else { + console.error("Opus 初始化失败"); + return false; + } + } catch (error) { + console.error("Opus 初始化失败:", error); + return false; + } +} + +// 将Float32音频数据转换为Int16音频数据 +function convertFloat32ToInt16(float32Data) { + const int16Data = new Int16Array(float32Data.length); + for (let i = 0; i < float32Data.length; i++) { + // 将[-1,1]范围转换为[-32768,32767] + const s = Math.max(-1, Math.min(1, float32Data[i])); + int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + return int16Data; +} + +// 将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; +} + +function startRecording() { + if (isRecording) return; + + // 确保有权限并且AudioContext是活跃的 + if (audioContext.state === 'suspended') { + audioContext.resume().then(() => { + console.log("AudioContext已恢复"); + continueStartRecording(); + }).catch(err => { + console.error("恢复AudioContext失败:", err); + statusLabel.textContent = "无法激活音频上下文,请再次点击"; + }); + } else { + continueStartRecording(); + } +} + +// 实际开始录音的逻辑 +function continueStartRecording() { + // 重置录音数据 + recordedPcmData = []; + recordedOpusData = []; + window.audioDataBuffer = new Int16Array(0); // 重置缓冲区 + + // 初始化Opus + initOpus().then(success => { + if (!success) { + statusLabel.textContent = "Opus初始化失败"; + return; + } + + console.log("开始录音,参数:", { + sampleRate: SAMPLE_RATE, + channels: CHANNELS, + frameSize: FRAME_SIZE, + bufferSize: BUFFER_SIZE + }); + + // 如果WebSocket已连接,发送开始录音信号 + if (isConnected && websocket && websocket.readyState === WebSocket.OPEN) { + sendVoiceControlMessage('start'); + } + + // 请求麦克风权限 + navigator.mediaDevices.getUserMedia({ + audio: { + sampleRate: SAMPLE_RATE, + channelCount: CHANNELS, + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true + } + }) + .then(stream => { + console.log("获取到麦克风流,实际参数:", stream.getAudioTracks()[0].getSettings()); + + // 检查流是否有效 + if (!stream || !stream.getAudioTracks().length || !stream.getAudioTracks()[0].enabled) { + throw new Error("获取到的音频流无效"); + } + + mediaStream = stream; + mediaSource = audioContext.createMediaStreamSource(stream); + + // 创建ScriptProcessor(虽然已弃用,但兼容性好) + // 在降级到ScriptProcessor之前尝试使用AudioWorklet + createAudioProcessor().then(processor => { + if (processor) { + console.log("使用AudioWorklet处理音频"); + audioProcessor = processor; + // 连接音频处理链 + mediaSource.connect(audioProcessor); + audioProcessor.connect(audioContext.destination); + } else { + console.log("回退到ScriptProcessor"); + // 创建ScriptProcessor节点 + audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, CHANNELS, CHANNELS); + + // 处理音频数据 + audioProcessor.onaudioprocess = processAudioData; + + // 连接音频处理链 + mediaSource.connect(audioProcessor); + audioProcessor.connect(audioContext.destination); + } + + // 更新UI + isRecording = true; + statusLabel.textContent = "录音中..."; + startButton.disabled = true; + stopButton.disabled = false; + playButton.disabled = true; + }).catch(error => { + console.error("创建音频处理器失败:", error); + statusLabel.textContent = "创建音频处理器失败"; + }); + }) + .catch(error => { + console.error("获取麦克风失败:", error); + statusLabel.textContent = "获取麦克风失败: " + error.message; + }); + }); +} + +// 创建AudioWorklet处理器 +async function createAudioProcessor() { + try { + // 尝试使用更现代的AudioWorklet API + if ('AudioWorklet' in window && 'AudioWorkletNode' in window) { + // 定义AudioWorklet处理器代码 + const workletCode = ` + class OpusRecorderProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.buffers = []; + this.frameSize = ${FRAME_SIZE}; + this.buffer = new Float32Array(this.frameSize); + this.bufferIndex = 0; + this.isRecording = false; + + this.port.onmessage = (event) => { + if (event.data.command === 'start') { + this.isRecording = true; + } else if (event.data.command === 'stop') { + this.isRecording = false; + // 发送最后的缓冲区 + if (this.bufferIndex > 0) { + const finalBuffer = this.buffer.slice(0, this.bufferIndex); + this.port.postMessage({ buffer: finalBuffer }); + } + } + }; + } + + process(inputs, outputs) { + if (!this.isRecording) return true; + + // 获取输入数据 + const input = inputs[0][0]; // mono channel + if (!input || input.length === 0) return true; + + // 将输入数据添加到缓冲区 + for (let i = 0; i < input.length; i++) { + this.buffer[this.bufferIndex++] = input[i]; + + // 当缓冲区填满时,发送给主线程 + if (this.bufferIndex >= this.frameSize) { + this.port.postMessage({ buffer: this.buffer.slice() }); + this.bufferIndex = 0; + } + } + + return true; + } + } + + registerProcessor('opus-recorder-processor', OpusRecorderProcessor); + `; + + // 创建Blob URL + const blob = new Blob([workletCode], { type: 'application/javascript' }); + const url = URL.createObjectURL(blob); + + // 加载AudioWorklet模块 + await audioContext.audioWorklet.addModule(url); + + // 创建AudioWorkletNode + const workletNode = new AudioWorkletNode(audioContext, 'opus-recorder-processor'); + + // 处理从AudioWorklet接收的消息 + workletNode.port.onmessage = (event) => { + if (event.data.buffer) { + // 使用与ScriptProcessor相同的处理逻辑 + processAudioData({ + inputBuffer: { + getChannelData: () => event.data.buffer + } + }); + } + }; + + // 启动录音 + workletNode.port.postMessage({ command: 'start' }); + + // 保存停止函数 + workletNode.stopRecording = () => { + workletNode.port.postMessage({ command: 'stop' }); + }; + + console.log("AudioWorklet 音频处理器创建成功"); + return workletNode; + } + } catch (error) { + console.error("创建AudioWorklet失败,将使用ScriptProcessor:", error); + } + + // 如果AudioWorklet不可用或失败,返回null以便回退到ScriptProcessor + return null; +} + +// 处理音频数据 +function processAudioData(e) { + // 获取输入缓冲区 + const inputBuffer = e.inputBuffer; + + // 获取第一个通道的Float32数据 + const inputData = inputBuffer.getChannelData(0); + + // 添加调试信息 + const nonZeroCount = Array.from(inputData).filter(x => Math.abs(x) > 0.001).length; + console.log(`接收到音频数据: ${inputData.length} 个样本, 非零样本数: ${nonZeroCount}`); + + // 如果全是0,可能是麦克风没有正确获取声音 + if (nonZeroCount < 5) { + console.warn("警告: 检测到大量静音样本,请检查麦克风是否正常工作"); + // 继续处理,以防有些样本确实是静音 + } + + // 存储PCM数据用于调试 + recordedPcmData.push(new Float32Array(inputData)); + + // 转换为Int16数据供Opus编码 + const int16Data = convertFloat32ToInt16(inputData); + + // 如果收集到的数据不是FRAME_SIZE的整数倍,需要进行处理 + // 创建静态缓冲区来存储不足一帧的数据 + if (!window.audioDataBuffer) { + window.audioDataBuffer = new Int16Array(0); + } + + // 合并之前缓存的数据和新数据 + const combinedData = new Int16Array(window.audioDataBuffer.length + int16Data.length); + combinedData.set(window.audioDataBuffer); + combinedData.set(int16Data, window.audioDataBuffer.length); + + // 处理完整帧 + const frameCount = Math.floor(combinedData.length / FRAME_SIZE); + console.log(`可编码的完整帧数: ${frameCount}, 缓冲区总大小: ${combinedData.length}`); + + for (let i = 0; i < frameCount; i++) { + const frameData = combinedData.subarray(i * FRAME_SIZE, (i + 1) * FRAME_SIZE); + + try { + console.log(`编码第 ${i+1}/${frameCount} 帧, 帧大小: ${frameData.length}`); + const encodedData = opusEncoder.encode(frameData); + if (encodedData) { + console.log(`编码成功: ${encodedData.length} 字节`); + recordedOpusData.push(encodedData); + + // 如果WebSocket已连接,发送编码后的数据 + if (isConnected && websocket && websocket.readyState === WebSocket.OPEN) { + sendOpusDataToServer(encodedData); + } + } + } catch (error) { + console.error(`Opus编码帧 ${i+1} 失败:`, error); + } + } + + // 保存剩余不足一帧的数据 + const remainingSamples = combinedData.length % FRAME_SIZE; + if (remainingSamples > 0) { + window.audioDataBuffer = combinedData.subarray(frameCount * FRAME_SIZE); + console.log(`保留 ${remainingSamples} 个样本到下一次处理`); + } else { + window.audioDataBuffer = new Int16Array(0); + } +} + +function stopRecording() { + if (!isRecording) return; + + // 处理剩余的缓冲数据 + if (window.audioDataBuffer && window.audioDataBuffer.length > 0) { + console.log(`停止录音,处理剩余的 ${window.audioDataBuffer.length} 个样本`); + // 如果剩余数据不足一帧,可以通过补零的方式凑成一帧 + if (window.audioDataBuffer.length < FRAME_SIZE) { + const paddedFrame = new Int16Array(FRAME_SIZE); + paddedFrame.set(window.audioDataBuffer); + // 剩余部分填充为0 + for (let i = window.audioDataBuffer.length; i < FRAME_SIZE; i++) { + paddedFrame[i] = 0; + } + try { + console.log(`编码最后一帧(补零): ${paddedFrame.length} 样本`); + const encodedData = opusEncoder.encode(paddedFrame); + if (encodedData) { + recordedOpusData.push(encodedData); + + // 如果WebSocket已连接,发送最后一帧 + if (isConnected && websocket && websocket.readyState === WebSocket.OPEN) { + sendOpusDataToServer(encodedData); + } + } + } catch (error) { + console.error("最后一帧Opus编码失败:", error); + } + } else { + // 如果数据超过一帧,按正常流程处理 + processAudioData({ + inputBuffer: { + getChannelData: () => convertInt16ToFloat32(window.audioDataBuffer) + } + }); + } + window.audioDataBuffer = null; + } + + // 如果WebSocket已连接,发送停止录音信号 + if (isConnected && websocket && websocket.readyState === WebSocket.OPEN) { + // 发送一个空帧作为结束标记 + const emptyFrame = new Uint8Array(0); + websocket.send(emptyFrame); + + // 发送停止录音控制消息 + sendVoiceControlMessage('stop'); + } + + // 如果使用的是AudioWorklet,调用其特定的停止方法 + if (audioProcessor && typeof audioProcessor.stopRecording === 'function') { + audioProcessor.stopRecording(); + } + + // 停止麦克风 + if (mediaStream) { + mediaStream.getTracks().forEach(track => track.stop()); + } + + // 断开音频处理链 + if (audioProcessor) { + try { + audioProcessor.disconnect(); + if (mediaSource) mediaSource.disconnect(); + } catch (error) { + console.warn("断开音频处理链时出错:", error); + } + } + + // 更新UI + isRecording = false; + statusLabel.textContent = "已停止录音,收集了 " + recordedOpusData.length + " 帧Opus数据"; + startButton.disabled = false; + stopButton.disabled = true; + playButton.disabled = recordedOpusData.length === 0; + + console.log("录制完成:", + "PCM帧数:", recordedPcmData.length, + "Opus帧数:", recordedOpusData.length); +} + +function playRecording() { + if (!recordedOpusData.length) { + statusLabel.textContent = "没有可播放的录音"; + return; + } + + // 将所有Opus数据解码为PCM + let allDecodedData = []; + + for (const opusData of recordedOpusData) { + try { + // 解码为Int16数据 + const decodedData = opusDecoder.decode(opusData); + + if (decodedData && decodedData.length > 0) { + // 将Int16数据转换为Float32 + const float32Data = convertInt16ToFloat32(decodedData); + + // 添加到总解码数据中 + allDecodedData.push(...float32Data); + } + } catch (error) { + console.error("Opus解码失败:", error); + } + } + + // 如果没有解码出数据,返回 + if (allDecodedData.length === 0) { + statusLabel.textContent = "解码失败,无法播放"; + return; + } + + // 创建音频缓冲区 + const audioBuffer = audioContext.createBuffer(CHANNELS, allDecodedData.length, SAMPLE_RATE); + audioBuffer.copyToChannel(new Float32Array(allDecodedData), 0); + + // 创建音频源并播放 + const source = audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(audioContext.destination); + source.start(); + + // 更新UI + statusLabel.textContent = "正在播放..."; + playButton.disabled = true; + + // 播放结束后恢复UI + source.onended = () => { + statusLabel.textContent = "播放完毕"; + playButton.disabled = false; + }; +} + +// 处理二进制消息的修改版本 +async function handleBinaryMessage(data) { + try { + let arrayBuffer; + + // 根据数据类型进行处理 + if (data instanceof ArrayBuffer) { + arrayBuffer = data; + console.log(`收到ArrayBuffer音频数据,大小: ${data.byteLength}字节`); + } else if (data instanceof Blob) { + // 如果是Blob类型,转换为ArrayBuffer + arrayBuffer = await data.arrayBuffer(); + console.log(`收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`); + } else { + console.warn(`收到未知类型的二进制数据: ${typeof data}`); + return; + } + + // 创建Uint8Array用于处理 + const opusData = new Uint8Array(arrayBuffer); + + if (opusData.length > 0) { + // 将数据添加到缓冲队列 + audioBufferQueue.push(opusData); + + // 如果收到的是第一个音频包,开始缓冲过程 + if (audioBufferQueue.length === 1 && !isAudioBuffering && !isAudioPlaying) { + startAudioBuffering(); + } + } else { + console.warn('收到空音频数据帧,可能是结束标志'); + + // 如果缓冲队列中有数据且没有在播放,立即开始播放 + if (audioBufferQueue.length > 0 && !isAudioPlaying) { + playBufferedAudio(); + } + + // 如果正在播放,发送结束信号 + if (isAudioPlaying && streamingContext) { + streamingContext.endOfStream = true; + } + } + } catch (error) { + console.error(`处理二进制消息出错:`, error); + } +} + +// 开始音频缓冲过程 +function startAudioBuffering() { + if (isAudioBuffering || isAudioPlaying) return; + + isAudioBuffering = true; + console.log("开始音频缓冲..."); + + // 设置超时,如果在一定时间内没有收集到足够的音频包,就开始播放 + setTimeout(() => { + if (isAudioBuffering && audioBufferQueue.length > 0) { + console.log(`缓冲超时,当前缓冲包数: ${audioBufferQueue.length},开始播放`); + playBufferedAudio(); + } + }, 300); // 300ms超时 + + // 监控缓冲进度 + const bufferCheckInterval = setInterval(() => { + if (!isAudioBuffering) { + clearInterval(bufferCheckInterval); + return; + } + + // 当累积了足够的音频包,开始播放 + if (audioBufferQueue.length >= BUFFER_THRESHOLD) { + clearInterval(bufferCheckInterval); + console.log(`已缓冲 ${audioBufferQueue.length} 个音频包,开始播放`); + playBufferedAudio(); + } + }, 50); +} + +// 播放已缓冲的音频 +function playBufferedAudio() { + if (isAudioPlaying || audioBufferQueue.length === 0) return; + + isAudioPlaying = true; + isAudioBuffering = false; + + // 创建流式播放上下文 + if (!streamingContext) { + streamingContext = { + queue: [], // 已解码的PCM队列 + playing: false, // 是否正在播放 + endOfStream: false, // 是否收到结束信号 + source: null, // 当前音频源 + totalSamples: 0, // 累积的总样本数 + lastPlayTime: 0, // 上次播放的时间戳 + // 将Opus数据解码为PCM + decodeOpusFrames: async function(opusFrames) { + 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) { + console.error("Opus解码失败:", 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(); + } + } + }, + // 开始播放音频 + 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; + console.log(`开始播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / SAMPLE_RATE).toFixed(2)} 秒`); + + // 播放结束后的处理 + 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) { + // 流已结束且没有更多数据 + console.log("音频播放完成"); + 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) { + // 真的没有更多数据了 + console.log("音频播放完成 (超时)"); + isAudioPlaying = false; + streamingContext = null; + } + }, 500); // 500ms超时 + } + }; + + this.source.start(); + } + }; + } + + // 开始处理缓冲的数据 + const frames = [...audioBufferQueue]; + audioBufferQueue = []; // 清空缓冲队列 + + // 解码并播放 + streamingContext.decodeOpusFrames(frames); +} + +// 将旧的playOpusFromServer函数保留为备用方法 +function playOpusFromServerOld(opusData) { + if (!opusDecoder) { + initOpus().then(success => { + if (success) { + decodeAndPlayOpusDataOld(opusData); + } else { + statusLabel.textContent = "Opus解码器初始化失败"; + } + }); + } else { + decodeAndPlayOpusDataOld(opusData); + } +} + +// 旧的解码和播放函数作为备用 +function decodeAndPlayOpusDataOld(opusData) { + let allDecodedData = []; + + for (const frame of opusData) { + try { + const decodedData = opusDecoder.decode(frame); + if (decodedData && decodedData.length > 0) { + const float32Data = convertInt16ToFloat32(decodedData); + allDecodedData.push(...float32Data); + } + } catch (error) { + console.error("服务端Opus数据解码失败:", error); + } + } + + if (allDecodedData.length === 0) { + statusLabel.textContent = "服务端数据解码失败"; + return; + } + + const audioBuffer = audioContext.createBuffer(CHANNELS, allDecodedData.length, SAMPLE_RATE); + audioBuffer.copyToChannel(new Float32Array(allDecodedData), 0); + + const source = audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(audioContext.destination); + source.start(); + + statusLabel.textContent = "正在播放服务端数据..."; + source.onended = () => statusLabel.textContent = "服务端数据播放完毕"; +} + +// 更新playOpusFromServer函数为Promise版本 +function playOpusFromServer(opusData) { + // 为了兼容,我们将opusData添加到audioBufferQueue并触发播放 + if (Array.isArray(opusData) && opusData.length > 0) { + for (const frame of opusData) { + audioBufferQueue.push(frame); + } + + // 如果没有在播放和缓冲,启动流程 + if (!isAudioBuffering && !isAudioPlaying) { + startAudioBuffering(); + } + + return new Promise(resolve => { + // 我们无法准确知道何时播放完成,所以设置一个合理的超时 + setTimeout(resolve, 1000); // 1秒后认为已处理 + }); + } else { + // 如果不是数组或为空,使用旧方法 + return new Promise(resolve => { + playOpusFromServerOld(opusData); + setTimeout(resolve, 1000); + }); + } +} + +// 连接WebSocket服务器 +function connectToServer() { + let url = serverUrlInput.value || "ws://127.0.0.1:8000/xiaozhi/v1/"; + + try { + // 检查URL格式 + if (!url.startsWith('ws://') && !url.startsWith('wss://')) { + console.error('URL格式错误,必须以ws://或wss://开头'); + updateStatus('URL格式错误,必须以ws://或wss://开头', 'error'); + return; + } + + // 添加认证参数 + let connUrl = new URL(url); + connUrl.searchParams.append('device_id', 'web_test_device'); + connUrl.searchParams.append('device_mac', '00:11:22:33:44:55'); + + console.log(`正在连接: ${connUrl.toString()}`); + updateStatus(`正在连接: ${connUrl.toString()}`, 'info'); + + websocket = new WebSocket(connUrl.toString()); + + // 设置接收二进制数据的类型为ArrayBuffer + websocket.binaryType = 'arraybuffer'; + + websocket.onopen = async () => { + console.log(`已连接到服务器: ${url}`); + updateStatus(`已连接到服务器: ${url}`, 'success'); + isConnected = true; + + // 连接成功后发送hello消息 + await sendHelloMessage(); + + if(connectButton.id === "connectButton") { + connectButton.textContent = '断开'; + // connectButton.onclick = disconnectFromServer; + connectButton.removeEventListener("click", connectToServer); + connectButton.addEventListener("click", disconnectFromServer); + } + + if(messageInput.id === "messageInput") { + messageInput.disabled = false; + } + + if(sendTextButton.id === "sendTextButton") { + sendTextButton.disabled = false; + } + }; + + websocket.onclose = () => { + console.log('已断开连接'); + updateStatus('已断开连接', 'info'); + isConnected = false; + + if(connectButton.id === "connectButton") { + connectButton.textContent = '连接'; + // connectButton.onclick = connectToServer; + connectButton.removeEventListener("click", disconnectFromServer); + connectButton.addEventListener("click", connectToServer); + } + + if(messageInput.id === "messageInput") { + messageInput.disabled = true; + } + + if(sendTextButton.id === "sendTextButton") { + sendTextButton.disabled = true; + } + }; + + websocket.onerror = (error) => { + console.error(`WebSocket错误:`, error); + updateStatus(`WebSocket错误`, 'error'); + }; + + websocket.onmessage = function (event) { + try { + // 检查是否为文本消息 + if (typeof event.data === 'string') { + const message = JSON.parse(event.data); + handleTextMessage(message); + } else { + // 处理二进制数据 + handleBinaryMessage(event.data); + } + } catch (error) { + console.error(`WebSocket消息处理错误:`, error); + // 非JSON格式文本消息直接显示 + if (typeof event.data === 'string') { + addMessage(event.data); + } + } + }; + + updateStatus('正在连接...', 'info'); + } catch (error) { + console.error(`连接错误:`, error); + updateStatus(`连接失败: ${error.message}`, 'error'); + } +} + +// 断开WebSocket连接 +function disconnectFromServer() { + if (!websocket) return; + + websocket.close(); + if (isRecording) { + stopRecording(); + } +} + +// 发送hello握手消息 +async function sendHelloMessage() { + if (!websocket || websocket.readyState !== WebSocket.OPEN) return; + + try { + // 设置设备信息 + const helloMessage = { + type: 'hello', + device_id: 'web_test_device', + device_name: 'Web测试设备', + device_mac: '00:11:22:33:44:55', + token: 'your-token1' // 使用config.yaml中配置的token + }; + + console.log('发送hello握手消息'); + websocket.send(JSON.stringify(helloMessage)); + + // 等待服务器响应 + return new Promise(resolve => { + // 5秒超时 + const timeout = setTimeout(() => { + console.error('等待hello响应超时'); + resolve(false); + }, 5000); + + // 临时监听一次消息,接收hello响应 + const onMessageHandler = (event) => { + try { + const response = JSON.parse(event.data); + if (response.type === 'hello' && response.session_id) { + console.log(`服务器握手成功,会话ID: ${response.session_id}`); + clearTimeout(timeout); + websocket.removeEventListener('message', onMessageHandler); + resolve(true); + } + } catch (e) { + // 忽略非JSON消息 + } + }; + + websocket.addEventListener('message', onMessageHandler); + }); + } catch (error) { + console.error(`发送hello消息错误:`, error); + return false; + } +} + +// 发送文本消息 +function sendTextMessage() { + const message = messageInput ? messageInput.value.trim() : ""; + if (message === '' || !websocket || websocket.readyState !== WebSocket.OPEN) return; + + try { + // 发送listen消息 + const listenMessage = { + type: 'listen', + mode: 'manual', + state: 'detect', + text: message + }; + + websocket.send(JSON.stringify(listenMessage)); + addMessage(message, true); + console.log(`发送文本消息: ${message}`); + + if (messageInput) { + messageInput.value = ''; + } + } catch (error) { + console.error(`发送消息错误:`, error); + } +} + +// 添加消息到会话记录 +function addMessage(text, isUser = false) { + if (!conversationDiv) return; + + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${isUser ? 'user' : 'server'}`; + messageDiv.textContent = text; + conversationDiv.appendChild(messageDiv); + conversationDiv.scrollTop = conversationDiv.scrollHeight; +} + +// 更新状态信息 +function updateStatus(message, type = 'info') { + console.log(`[${type}] ${message}`); + if (statusLabel) { + statusLabel.textContent = message; + } + if (connectionStatus) { + connectionStatus.textContent = message; + switch(type) { + case 'success': + connectionStatus.style.color = 'green'; + break; + case 'error': + connectionStatus.style.color = 'red'; + break; + case 'info': + default: + connectionStatus.style.color = 'black'; + break; + } + } +} + +// 处理文本消息 +function handleTextMessage(message) { + if (message.type === 'hello') { + console.log(`服务器回应:${JSON.stringify(message, null, 2)}`); + } else if (message.type === 'tts') { + // TTS状态消息 + if (message.state === 'start') { + console.log('服务器开始发送语音'); + } else if (message.state === 'sentence_start') { + console.log(`服务器发送语音段: ${message.text}`); + // 添加文本到会话记录 + if (message.text) { + addMessage(message.text); + } + } else if (message.state === 'sentence_end') { + console.log(`语音段结束: ${message.text}`); + } else if (message.state === 'stop') { + console.log('服务器语音传输结束'); + } + } else if (message.type === 'audio') { + // 音频控制消息 + console.log(`收到音频控制消息: ${JSON.stringify(message)}`); + } else if (message.type === 'stt') { + // 语音识别结果 + console.log(`识别结果: ${message.text}`); + // 添加识别结果到会话记录 + addMessage(`[语音识别] ${message.text}`, true); + } else if (message.type === 'llm') { + // 大模型回复 + console.log(`大模型回复: ${message.text}`); + // 添加大模型回复到会话记录 + if (message.text && message.text !== '😊') { + addMessage(message.text); + } + } else { + // 未知消息类型 + console.log(`未知消息类型: ${message.type}`); + addMessage(JSON.stringify(message, null, 2)); + } +} + +// 发送语音数据到WebSocket +function sendOpusDataToServer(opusData) { + if (!websocket || websocket.readyState !== WebSocket.OPEN) { + console.error('WebSocket未连接,无法发送音频数据'); + return false; + } + + try { + // 发送二进制数据 + websocket.send(opusData.buffer); + console.log(`已发送Opus音频数据: ${opusData.length}字节`); + return true; + } catch (error) { + console.error(`发送音频数据失败:`, error); + return false; + } +} + +// 发送语音开始和结束信号 +function sendVoiceControlMessage(state) { + if (!websocket || websocket.readyState !== WebSocket.OPEN) return; + + try { + const message = { + type: 'listen', + mode: 'manual', + state: state // 'start' 或 'stop' + }; + + websocket.send(JSON.stringify(message)); + console.log(`发送语音${state === 'start' ? '开始' : '结束'}控制消息`); + } catch (error) { + console.error(`发送语音控制消息失败:`, error); + } +} diff --git a/vue/apps/bot_web_test/abbreviated_version/test.html b/vue/apps/bot_web_test/abbreviated_version/test.html new file mode 100644 index 0000000..498c57c --- /dev/null +++ b/vue/apps/bot_web_test/abbreviated_version/test.html @@ -0,0 +1,503 @@ + + +
+ +录音状态: 待机,正在初始化...
+>>16&4;E=E<>>16&2;i=14-(p|q|i)+(E<>>15)|0;i=o>>>(i+7|0)&1|i<<1}else i=0;b=c[30880+(i<<2)>>2]|0;a:do if(!b){a=0;b=0;E=86}else{f=d;a=0;g=o<<((i|0)==31?0:25-(i>>>1)|0);h=b;b=0;while(1){e=c[h+4>>2]&-8;d=e-o|0;if(d>>>0
>>0)if((e|0)==(o|0)){a=h;b=h;E=90;break a}else b=h;else d=f;e=c[h+20>>2]|0;h=c[h+16+(g>>>31<<2)>>2]|0;a=(e|0)==0|(e|0)==(h|0)?a:e;e=(h|0)==0;if(e){E=86;break}else{f=d;g=g<<(e&1^1)}}}while(0);if((E|0)==86){if((a|0)==0&(b|0)==0){a=2<>>12&16;q=q>>>m;l=q>>>5&8;q=q>>>l;n=q>>>2&4;q=q>>>n;p=q>>>1&2;q=q>>>p;a=q>>>1&1;a=c[30880+((l|m|n|p|a)+(q>>>a)<<2)>>2]|0}if(!a){i=d;j=b}else E=90}if((E|0)==90)while(1){E=0;q=(c[a+4>>2]&-8)-o|0;e=q>>>0 >>0;d=e?q:d;b=e?a:b;e=c[a+16>>2]|0;if(e|0){a=e;E=90;continue}a=c[a+20>>2]|0;if(!a){i=d;j=b;break}else E=90}if((j|0)!=0?i>>>0<((c[7646]|0)-o|0)>>>0:0){f=c[7648]|0;if(j>>>0 >>0)ga();h=j+o|0;if(j>>>0>=h>>>0)ga();g=c[j+24>>2]|0;d=c[j+12>>2]|0;do if((d|0)==(j|0)){b=j+20|0;a=c[b>>2]|0;if(!a){b=j+16|0;a=c[b>>2]|0;if(!a){s=0;break}}while(1){d=a+20|0;e=c[d>>2]|0;if(e|0){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0 >>0)ga();else{c[b>>2]=0;s=a;break}}else{e=c[j+8>>2]|0;if(e>>>0 >>0)ga();a=e+12|0;if((c[a>>2]|0)!=(j|0))ga();b=d+8|0;if((c[b>>2]|0)==(j|0)){c[a>>2]=d;c[b>>2]=e;s=d;break}else ga()}while(0);do if(g|0){a=c[j+28>>2]|0;b=30880+(a<<2)|0;if((j|0)==(c[b>>2]|0)){c[b>>2]=s;if(!s){c[7645]=c[7645]&~(1<>>0<(c[7648]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(j|0))c[a>>2]=s;else c[g+20>>2]=s;if(!s)break}b=c[7648]|0;if(s>>>0>>0)ga();c[s+24>>2]=g;a=c[j+16>>2]|0;do if(a|0)if(a>>>0>>0)ga();else{c[s+16>>2]=a;c[a+24>>2]=s;break}while(0);a=c[j+20>>2]|0;if(a|0)if(a>>>0<(c[7648]|0)>>>0)ga();else{c[s+20>>2]=a;c[a+24>>2]=s;break}}while(0);do if(i>>>0>=16){c[j+4>>2]=o|3;c[h+4>>2]=i|1;c[h+i>>2]=i;a=i>>>3;if(i>>>0<256){d=30616+(a<<1<<2)|0;b=c[7644]|0;a=1<>2]|0;if(b>>>0<(c[7648]|0)>>>0)ga();else{u=a;v=b}}else{c[7644]=b|a;u=d+8|0;v=d}c[u>>2]=h;c[v+12>>2]=h;c[h+8>>2]=v;c[h+12>>2]=d;break}a=i>>>8;if(a)if(i>>>0>16777215)d=31;else{K=(a+1048320|0)>>>16&8;L=a< >>16&4;L=L< >>16&2;d=14-(J|K|d)+(L< >>15)|0;d=i>>>(d+7|0)&1|d<<1}else d=0;e=30880+(d<<2)|0;c[h+28>>2]=d;a=h+16|0;c[a+4>>2]=0;c[a>>2]=0;a=c[7645]|0;b=1< >2]=h;c[h+24>>2]=e;c[h+12>>2]=h;c[h+8>>2]=h;break}f=i<<((d|0)==31?0:25-(d>>>1)|0);a=c[e>>2]|0;while(1){if((c[a+4>>2]&-8|0)==(i|0)){d=a;E=148;break}b=a+16+(f>>>31<<2)|0;d=c[b>>2]|0;if(!d){E=145;break}else{f=f<<1;a=d}}if((E|0)==145)if(b>>>0<(c[7648]|0)>>>0)ga();else{c[b>>2]=h;c[h+24>>2]=a;c[h+12>>2]=h;c[h+8>>2]=h;break}else if((E|0)==148){a=d+8|0;b=c[a>>2]|0;L=c[7648]|0;if(b>>>0>=L>>>0&d>>>0>=L>>>0){c[b+12>>2]=h;c[a>>2]=h;c[h+8>>2]=b;c[h+12>>2]=d;c[h+24>>2]=0;break}else ga()}}else{L=i+o|0;c[j+4>>2]=L|3;L=j+L+4|0;c[L>>2]=c[L>>2]|1}while(0);L=j+8|0;return L|0}}}else o=-1;while(0);d=c[7646]|0;if(d>>>0>=o>>>0){a=d-o|0;b=c[7649]|0;if(a>>>0>15){L=b+o|0;c[7649]=L;c[7646]=a;c[L+4>>2]=a|1;c[L+a>>2]=a;c[b+4>>2]=o|3}else{c[7646]=0;c[7649]=0;c[b+4>>2]=d|3;L=b+d+4|0;c[L>>2]=c[L>>2]|1}L=b+8|0;return L|0}a=c[7647]|0;if(a>>>0>o>>>0){J=a-o|0;c[7647]=J;L=c[7650]|0;K=L+o|0;c[7650]=K;c[K+4>>2]=J|1;c[L+4>>2]=o|3;L=L+8|0;return L|0}do if(!(c[7762]|0)){a=oa(30)|0;if(!(a+-1&a)){c[7764]=a;c[7763]=a;c[7765]=-1;c[7766]=-1;c[7767]=0;c[7755]=0;c[7762]=(ka(0)|0)&-16^1431655768;break}else ga()}while(0);h=o+48|0;g=c[7764]|0;i=o+47|0;f=g+i|0;g=0-g|0;j=f&g;if(j>>>0<=o>>>0){L=0;return L|0}a=c[7754]|0;if(a|0?(u=c[7752]|0,v=u+j|0,v>>>0<=u>>>0|v>>>0>a>>>0):0){L=0;return L|0}b:do if(!(c[7755]&4)){a=c[7650]|0;c:do if(a){d=31024;while(1){b=c[d>>2]|0;if(b>>>0<=a>>>0?(r=d+4|0,(b+(c[r>>2]|0)|0)>>>0>a>>>0):0){e=d;d=r;break}d=c[d+8>>2]|0;if(!d){E=173;break c}}a=f-(c[7647]|0)&g;if(a>>>0<2147483647){b=ja(a|0)|0;if((b|0)==((c[e>>2]|0)+(c[d>>2]|0)|0)){if((b|0)!=(-1|0)){h=b;f=a;E=193;break b}}else E=183}}else E=173;while(0);do if((E|0)==173?(t=ja(0)|0,(t|0)!=(-1|0)):0){a=t;b=c[7763]|0;d=b+-1|0;if(!(d&a))a=j;else a=j-a+(d+a&0-b)|0;b=c[7752]|0;d=b+a|0;if(a>>>0>o>>>0&a>>>0<2147483647){v=c[7754]|0;if(v|0?d>>>0<=b>>>0|d>>>0>v>>>0:0)break;b=ja(a|0)|0;if((b|0)==(t|0)){h=t;f=a;E=193;break b}else E=183}}while(0);d:do if((E|0)==183){d=0-a|0;do if(h>>>0>a>>>0&(a>>>0<2147483647&(b|0)!=(-1|0))?(w=c[7764]|0,w=i-a+w&0-w,w>>>0<2147483647):0)if((ja(w|0)|0)==(-1|0)){ja(d|0)|0;break d}else{a=w+a|0;break}while(0);if((b|0)!=(-1|0)){h=b;f=a;E=193;break b}}while(0);c[7755]=c[7755]|4;E=190}else E=190;while(0);if((((E|0)==190?j>>>0<2147483647:0)?(x=ja(j|0)|0,y=ja(0)|0,x>>>0 >>0&((x|0)!=(-1|0)&(y|0)!=(-1|0))):0)?(z=y-x|0,z>>>0>(o+40|0)>>>0):0){h=x;f=z;E=193}if((E|0)==193){a=(c[7752]|0)+f|0;c[7752]=a;if(a>>>0>(c[7753]|0)>>>0)c[7753]=a;i=c[7650]|0;do if(i){e=31024;do{a=c[e>>2]|0;b=e+4|0;d=c[b>>2]|0;if((h|0)==(a+d|0)){A=a;B=b;C=d;D=e;E=203;break}e=c[e+8>>2]|0}while((e|0)!=0);if(((E|0)==203?(c[D+12>>2]&8|0)==0:0)?i>>>0 >>0&i>>>0>=A>>>0:0){c[B>>2]=C+f;L=i+8|0;L=(L&7|0)==0?0:0-L&7;K=i+L|0;L=f-L+(c[7647]|0)|0;c[7650]=K;c[7647]=L;c[K+4>>2]=L|1;c[K+L+4>>2]=40;c[7651]=c[7766];break}a=c[7648]|0;if(h>>>0>>0){c[7648]=h;j=h}else j=a;d=h+f|0;a=31024;while(1){if((c[a>>2]|0)==(d|0)){b=a;E=211;break}a=c[a+8>>2]|0;if(!a){b=31024;break}}if((E|0)==211)if(!(c[a+12>>2]&8)){c[b>>2]=h;l=a+4|0;c[l>>2]=(c[l>>2]|0)+f;l=h+8|0;l=h+((l&7|0)==0?0:0-l&7)|0;a=d+8|0;a=d+((a&7|0)==0?0:0-a&7)|0;k=l+o|0;g=a-l-o|0;c[l+4>>2]=o|3;do if((a|0)!=(i|0)){if((a|0)==(c[7649]|0)){L=(c[7646]|0)+g|0;c[7646]=L;c[7649]=k;c[k+4>>2]=L|1;c[k+L>>2]=L;break}b=c[a+4>>2]|0;if((b&3|0)==1){i=b&-8;f=b>>>3;e:do if(b>>>0>=256){h=c[a+24>>2]|0;e=c[a+12>>2]|0;do if((e|0)==(a|0)){d=a+16|0;e=d+4|0;b=c[e>>2]|0;if(!b){b=c[d>>2]|0;if(!b){J=0;break}}else d=e;while(1){e=b+20|0;f=c[e>>2]|0;if(f|0){b=f;d=e;continue}e=b+16|0;f=c[e>>2]|0;if(!f)break;else{b=f;d=e}}if(d>>>0 >>0)ga();else{c[d>>2]=0;J=b;break}}else{f=c[a+8>>2]|0;if(f>>>0 >>0)ga();b=f+12|0;if((c[b>>2]|0)!=(a|0))ga();d=e+8|0;if((c[d>>2]|0)==(a|0)){c[b>>2]=e;c[d>>2]=f;J=e;break}else ga()}while(0);if(!h)break;b=c[a+28>>2]|0;d=30880+(b<<2)|0;do if((a|0)!=(c[d>>2]|0)){if(h>>>0<(c[7648]|0)>>>0)ga();b=h+16|0;if((c[b>>2]|0)==(a|0))c[b>>2]=J;else c[h+20>>2]=J;if(!J)break e}else{c[d>>2]=J;if(J|0)break;c[7645]=c[7645]&~(1<>>0 >>0)ga();c[J+24>>2]=h;b=a+16|0;d=c[b>>2]|0;do if(d|0)if(d>>>0 >>0)ga();else{c[J+16>>2]=d;c[d+24>>2]=J;break}while(0);b=c[b+4>>2]|0;if(!b)break;if(b>>>0<(c[7648]|0)>>>0)ga();else{c[J+20>>2]=b;c[b+24>>2]=J;break}}else{d=c[a+8>>2]|0;e=c[a+12>>2]|0;b=30616+(f<<1<<2)|0;do if((d|0)!=(b|0)){if(d>>>0 >>0)ga();if((c[d+12>>2]|0)==(a|0))break;ga()}while(0);if((e|0)==(d|0)){c[7644]=c[7644]&~(1< >>0 >>0)ga();b=e+8|0;if((c[b>>2]|0)==(a|0)){G=b;break}ga()}while(0);c[d+12>>2]=e;c[G>>2]=d}while(0);a=a+i|0;g=i+g|0}a=a+4|0;c[a>>2]=c[a>>2]&-2;c[k+4>>2]=g|1;c[k+g>>2]=g;a=g>>>3;if(g>>>0<256){d=30616+(a<<1<<2)|0;b=c[7644]|0;a=1<>2]|0;if(b>>>0>=(c[7648]|0)>>>0){K=a;L=b;break}ga()}while(0);c[K>>2]=k;c[L+12>>2]=k;c[k+8>>2]=L;c[k+12>>2]=d;break}a=g>>>8;do if(!a)d=0;else{if(g>>>0>16777215){d=31;break}K=(a+1048320|0)>>>16&8;L=a< >>16&4;L=L< >>16&2;d=14-(J|K|d)+(L< >>15)|0;d=g>>>(d+7|0)&1|d<<1}while(0);e=30880+(d<<2)|0;c[k+28>>2]=d;a=k+16|0;c[a+4>>2]=0;c[a>>2]=0;a=c[7645]|0;b=1< >2]=k;c[k+24>>2]=e;c[k+12>>2]=k;c[k+8>>2]=k;break}f=g<<((d|0)==31?0:25-(d>>>1)|0);a=c[e>>2]|0;while(1){if((c[a+4>>2]&-8|0)==(g|0)){d=a;E=281;break}b=a+16+(f>>>31<<2)|0;d=c[b>>2]|0;if(!d){E=278;break}else{f=f<<1;a=d}}if((E|0)==278)if(b>>>0<(c[7648]|0)>>>0)ga();else{c[b>>2]=k;c[k+24>>2]=a;c[k+12>>2]=k;c[k+8>>2]=k;break}else if((E|0)==281){a=d+8|0;b=c[a>>2]|0;L=c[7648]|0;if(b>>>0>=L>>>0&d>>>0>=L>>>0){c[b+12>>2]=k;c[a>>2]=k;c[k+8>>2]=b;c[k+12>>2]=d;c[k+24>>2]=0;break}else ga()}}else{L=(c[7647]|0)+g|0;c[7647]=L;c[7650]=k;c[k+4>>2]=L|1}while(0);L=l+8|0;return L|0}else b=31024;while(1){a=c[b>>2]|0;if(a>>>0<=i>>>0?(F=a+(c[b+4>>2]|0)|0,F>>>0>i>>>0):0){b=F;break}b=c[b+8>>2]|0}g=b+-47|0;d=g+8|0;d=g+((d&7|0)==0?0:0-d&7)|0;g=i+16|0;d=d>>>0 >>0?i:d;a=d+8|0;e=h+8|0;e=(e&7|0)==0?0:0-e&7;L=h+e|0;e=f+-40-e|0;c[7650]=L;c[7647]=e;c[L+4>>2]=e|1;c[L+e+4>>2]=40;c[7651]=c[7766];e=d+4|0;c[e>>2]=27;c[a>>2]=c[7756];c[a+4>>2]=c[7757];c[a+8>>2]=c[7758];c[a+12>>2]=c[7759];c[7756]=h;c[7757]=f;c[7759]=0;c[7758]=a;a=d+24|0;do{a=a+4|0;c[a>>2]=7}while((a+4|0)>>>0>>0);if((d|0)!=(i|0)){h=d-i|0;c[e>>2]=c[e>>2]&-2;c[i+4>>2]=h|1;c[d>>2]=h;a=h>>>3;if(h>>>0<256){d=30616+(a<<1<<2)|0;b=c[7644]|0;a=1<>2]|0;if(b>>>0<(c[7648]|0)>>>0)ga();else{H=a;I=b}}else{c[7644]=b|a;H=d+8|0;I=d}c[H>>2]=i;c[I+12>>2]=i;c[i+8>>2]=I;c[i+12>>2]=d;break}a=h>>>8;if(a)if(h>>>0>16777215)d=31;else{K=(a+1048320|0)>>>16&8;L=a< >>16&4;L=L< >>16&2;d=14-(J|K|d)+(L< >>15)|0;d=h>>>(d+7|0)&1|d<<1}else d=0;f=30880+(d<<2)|0;c[i+28>>2]=d;c[i+20>>2]=0;c[g>>2]=0;a=c[7645]|0;b=1< >2]=i;c[i+24>>2]=f;c[i+12>>2]=i;c[i+8>>2]=i;break}e=h<<((d|0)==31?0:25-(d>>>1)|0);a=c[f>>2]|0;while(1){if((c[a+4>>2]&-8|0)==(h|0)){d=a;E=307;break}b=a+16+(e>>>31<<2)|0;d=c[b>>2]|0;if(!d){E=304;break}else{e=e<<1;a=d}}if((E|0)==304)if(b>>>0<(c[7648]|0)>>>0)ga();else{c[b>>2]=i;c[i+24>>2]=a;c[i+12>>2]=i;c[i+8>>2]=i;break}else if((E|0)==307){a=d+8|0;b=c[a>>2]|0;L=c[7648]|0;if(b>>>0>=L>>>0&d>>>0>=L>>>0){c[b+12>>2]=i;c[a>>2]=i;c[i+8>>2]=b;c[i+12>>2]=d;c[i+24>>2]=0;break}else ga()}}}else{L=c[7648]|0;if((L|0)==0|h>>>0 >>0)c[7648]=h;c[7756]=h;c[7757]=f;c[7759]=0;c[7653]=c[7762];c[7652]=-1;a=0;do{L=30616+(a<<1<<2)|0;c[L+12>>2]=L;c[L+8>>2]=L;a=a+1|0}while((a|0)!=32);L=h+8|0;L=(L&7|0)==0?0:0-L&7;K=h+L|0;L=f+-40-L|0;c[7650]=K;c[7647]=L;c[K+4>>2]=L|1;c[K+L+4>>2]=40;c[7651]=c[7766]}while(0);a=c[7647]|0;if(a>>>0>o>>>0){J=a-o|0;c[7647]=J;L=c[7650]|0;K=L+o|0;c[7650]=K;c[K+4>>2]=J|1;c[L+4>>2]=o|3;L=L+8|0;return L|0}}c[(fj()|0)>>2]=12;L=0;return L|0}function kj(a){a=a|0;var b=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0;if(!a)return;d=a+-8|0;h=c[7648]|0;if(d>>>0 >>0)ga();a=c[a+-4>>2]|0;b=a&3;if((b|0)==1)ga();e=a&-8;m=d+e|0;do if(!(a&1)){a=c[d>>2]|0;if(!b)return;k=d+(0-a)|0;j=a+e|0;if(k>>>0 >>0)ga();if((k|0)==(c[7649]|0)){a=m+4|0;b=c[a>>2]|0;if((b&3|0)!=3){q=k;g=j;break}c[7646]=j;c[a>>2]=b&-2;c[k+4>>2]=j|1;c[k+j>>2]=j;return}e=a>>>3;if(a>>>0<256){b=c[k+8>>2]|0;d=c[k+12>>2]|0;a=30616+(e<<1<<2)|0;if((b|0)!=(a|0)){if(b>>>0 >>0)ga();if((c[b+12>>2]|0)!=(k|0))ga()}if((d|0)==(b|0)){c[7644]=c[7644]&~(1< >>0 >>0)ga();a=d+8|0;if((c[a>>2]|0)==(k|0))f=a;else ga()}else f=d+8|0;c[b+12>>2]=d;c[f>>2]=b;q=k;g=j;break}f=c[k+24>>2]|0;d=c[k+12>>2]|0;do if((d|0)==(k|0)){b=k+16|0;d=b+4|0;a=c[d>>2]|0;if(!a){a=c[b>>2]|0;if(!a){i=0;break}}else b=d;while(1){d=a+20|0;e=c[d>>2]|0;if(e|0){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0 >>0)ga();else{c[b>>2]=0;i=a;break}}else{e=c[k+8>>2]|0;if(e>>>0 >>0)ga();a=e+12|0;if((c[a>>2]|0)!=(k|0))ga();b=d+8|0;if((c[b>>2]|0)==(k|0)){c[a>>2]=d;c[b>>2]=e;i=d;break}else ga()}while(0);if(f){a=c[k+28>>2]|0;b=30880+(a<<2)|0;if((k|0)==(c[b>>2]|0)){c[b>>2]=i;if(!i){c[7645]=c[7645]&~(1<>>0<(c[7648]|0)>>>0)ga();a=f+16|0;if((c[a>>2]|0)==(k|0))c[a>>2]=i;else c[f+20>>2]=i;if(!i){q=k;g=j;break}}d=c[7648]|0;if(i>>>0 >>0)ga();c[i+24>>2]=f;a=k+16|0;b=c[a>>2]|0;do if(b|0)if(b>>>0 >>0)ga();else{c[i+16>>2]=b;c[b+24>>2]=i;break}while(0);a=c[a+4>>2]|0;if(a)if(a>>>0<(c[7648]|0)>>>0)ga();else{c[i+20>>2]=a;c[a+24>>2]=i;q=k;g=j;break}else{q=k;g=j}}else{q=k;g=j}}else{q=d;g=e}while(0);if(q>>>0>=m>>>0)ga();a=m+4|0;b=c[a>>2]|0;if(!(b&1))ga();if(!(b&2)){if((m|0)==(c[7650]|0)){p=(c[7647]|0)+g|0;c[7647]=p;c[7650]=q;c[q+4>>2]=p|1;if((q|0)!=(c[7649]|0))return;c[7649]=0;c[7646]=0;return}if((m|0)==(c[7649]|0)){p=(c[7646]|0)+g|0;c[7646]=p;c[7649]=q;c[q+4>>2]=p|1;c[q+p>>2]=p;return}g=(b&-8)+g|0;e=b>>>3;do if(b>>>0>=256){f=c[m+24>>2]|0;a=c[m+12>>2]|0;do if((a|0)==(m|0)){b=m+16|0;d=b+4|0;a=c[d>>2]|0;if(!a){a=c[b>>2]|0;if(!a){n=0;break}}else b=d;while(1){d=a+20|0;e=c[d>>2]|0;if(e|0){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<(c[7648]|0)>>>0)ga();else{c[b>>2]=0;n=a;break}}else{b=c[m+8>>2]|0;if(b>>>0<(c[7648]|0)>>>0)ga();d=b+12|0;if((c[d>>2]|0)!=(m|0))ga();e=a+8|0;if((c[e>>2]|0)==(m|0)){c[d>>2]=a;c[e>>2]=b;n=a;break}else ga()}while(0);if(f|0){a=c[m+28>>2]|0;b=30880+(a<<2)|0;if((m|0)==(c[b>>2]|0)){c[b>>2]=n;if(!n){c[7645]=c[7645]&~(1<>>0<(c[7648]|0)>>>0)ga();a=f+16|0;if((c[a>>2]|0)==(m|0))c[a>>2]=n;else c[f+20>>2]=n;if(!n)break}d=c[7648]|0;if(n>>>0 >>0)ga();c[n+24>>2]=f;a=m+16|0;b=c[a>>2]|0;do if(b|0)if(b>>>0 >>0)ga();else{c[n+16>>2]=b;c[b+24>>2]=n;break}while(0);a=c[a+4>>2]|0;if(a|0)if(a>>>0<(c[7648]|0)>>>0)ga();else{c[n+20>>2]=a;c[a+24>>2]=n;break}}}else{b=c[m+8>>2]|0;d=c[m+12>>2]|0;a=30616+(e<<1<<2)|0;if((b|0)!=(a|0)){if(b>>>0<(c[7648]|0)>>>0)ga();if((c[b+12>>2]|0)!=(m|0))ga()}if((d|0)==(b|0)){c[7644]=c[7644]&~(1< >>0<(c[7648]|0)>>>0)ga();a=d+8|0;if((c[a>>2]|0)==(m|0))l=a;else ga()}else l=d+8|0;c[b+12>>2]=d;c[l>>2]=b}while(0);c[q+4>>2]=g|1;c[q+g>>2]=g;if((q|0)==(c[7649]|0)){c[7646]=g;return}}else{c[a>>2]=b&-2;c[q+4>>2]=g|1;c[q+g>>2]=g}a=g>>>3;if(g>>>0<256){d=30616+(a<<1<<2)|0;b=c[7644]|0;a=1<>2]|0;if(b>>>0<(c[7648]|0)>>>0)ga();else{o=a;p=b}}else{c[7644]=b|a;o=d+8|0;p=d}c[o>>2]=q;c[p+12>>2]=q;c[q+8>>2]=p;c[q+12>>2]=d;return}a=g>>>8;if(a)if(g>>>0>16777215)d=31;else{o=(a+1048320|0)>>>16&8;p=a< >>16&4;p=p< >>16&2;d=14-(n|o|d)+(p< >>15)|0;d=g>>>(d+7|0)&1|d<<1}else d=0;e=30880+(d<<2)|0;c[q+28>>2]=d;c[q+20>>2]=0;c[q+16>>2]=0;a=c[7645]|0;b=1< >>1)|0);a=c[e>>2]|0;while(1){if((c[a+4>>2]&-8|0)==(g|0)){d=a;e=130;break}b=a+16+(f>>>31<<2)|0;d=c[b>>2]|0;if(!d){e=127;break}else{f=f<<1;a=d}}if((e|0)==127)if(b>>>0<(c[7648]|0)>>>0)ga();else{c[b>>2]=q;c[q+24>>2]=a;c[q+12>>2]=q;c[q+8>>2]=q;break}else if((e|0)==130){a=d+8|0;b=c[a>>2]|0;p=c[7648]|0;if(b>>>0>=p>>>0&d>>>0>=p>>>0){c[b+12>>2]=q;c[a>>2]=q;c[q+8>>2]=b;c[q+12>>2]=d;c[q+24>>2]=0;break}else ga()}}else{c[7645]=a|b;c[e>>2]=q;c[q+24>>2]=e;c[q+12>>2]=q;c[q+8>>2]=q}while(0);q=(c[7652]|0)+-1|0;c[7652]=q;if(!q)a=31032;else return;while(1){a=c[a>>2]|0;if(!a)break;else a=a+8|0}c[7652]=-1;return}function lj(){}function mj(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;c=a+c>>>0;return (C=b+d+(c>>>0>>0|0)>>>0,c|0)|0}function nj(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){C=b>>c;return a>>>c|(b&(1< >c-32|0}function oj(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;f=b+e|0;if((e|0)>=20){d=d&255;h=b&3;i=d|d<<8|d<<16|d<<24;g=f&~3;if(h){h=b+4-h|0;while((b|0)<(h|0)){a[b>>0]=d;b=b+1|0}}while((b|0)<(g|0)){c[b>>2]=i;b=b+4|0}}while((b|0)<(f|0)){a[b>>0]=d;b=b+1|0}return b-e|0}function pj(b,d,e){b=b|0;d=d|0;e=e|0;var f=0;if((e|0)>=4096)return ma(b|0,d|0,e|0)|0;f=b|0;if((b&3)==(d&3)){while(b&3){if(!e)return f|0;a[b>>0]=a[d>>0]|0;b=b+1|0;d=d+1|0;e=e-1|0}while((e|0)>=4){c[b>>2]=c[d>>2];b=b+4|0;d=d+4|0;e=e-4|0}}while((e|0)>0){a[b>>0]=a[d>>0]|0;b=b+1|0;d=d+1|0;e=e-1|0}return f|0}function qj(b,c,d){b=b|0;c=c|0;d=d|0;var e=0;if((c|0)<(b|0)&(b|0)<(c+d|0)){e=b;c=c+d|0;b=b+d|0;while((d|0)>0){b=b-1|0;c=c-1|0;d=d-1|0;a[b>>0]=a[c>>0]|0}b=e}else pj(b,c,d)|0;return b|0}function rj(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;d=b-d-(c>>>0>a>>>0|0)>>>0;return (C=d,a-c>>>0|0)|0}function sj(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){C=b< >>32-c;return a< >>c;return a>>>c|(b&(1< >>c-32|0}function uj(b){b=b|0;var c=0;c=a[m+(b&255)>>0]|0;if((c|0)<8)return c|0;c=a[m+(b>>8&255)>>0]|0;if((c|0)<8)return c+8|0;c=a[m+(b>>16&255)>>0]|0;if((c|0)<8)return c+16|0;return (a[m+(b>>>24)>>0]|0)+24|0}function vj(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0;f=a&65535;e=b&65535;c=_(e,f)|0;d=a>>>16;a=(c>>>16)+(_(e,d)|0)|0;e=b>>>16;b=_(e,f)|0;return (C=(a>>>16)+(_(e,d)|0)+(((a&65535)+b|0)>>>16)|0,a+b<<16|c&65535|0)|0}function wj(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0;j=b>>31|((b|0)<0?-1:0)<<1;i=((b|0)<0?-1:0)>>31|((b|0)<0?-1:0)<<1;f=d>>31|((d|0)<0?-1:0)<<1;e=((d|0)<0?-1:0)>>31|((d|0)<0?-1:0)<<1;h=rj(j^a|0,i^b|0,j|0,i|0)|0;g=C;a=f^j;b=e^i;return rj((Bj(h,g,rj(f^c|0,e^d|0,f|0,e|0)|0,C,0)|0)^a|0,C^b|0,a|0,b|0)|0}function xj(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0;f=i;i=i+16|0;j=f|0;h=b>>31|((b|0)<0?-1:0)<<1;g=((b|0)<0?-1:0)>>31|((b|0)<0?-1:0)<<1;l=e>>31|((e|0)<0?-1:0)<<1;k=((e|0)<0?-1:0)>>31|((e|0)<0?-1:0)<<1;a=rj(h^a|0,g^b|0,h|0,g|0)|0;b=C;Bj(a,b,rj(l^d|0,k^e|0,l|0,k|0)|0,C,j)|0;e=rj(c[j>>2]^h|0,c[j+4>>2]^g|0,h|0,g|0)|0;d=C;i=f;return (C=d,e)|0}function yj(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0;e=a;f=c;c=vj(e,f)|0;a=C;return (C=(_(b,f)|0)+(_(d,e)|0)+a|a&0,c|0|0)|0}function zj(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return Bj(a,b,c,d,0)|0}function Aj(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,g=0;g=i;i=i+16|0;f=g|0;Bj(a,b,d,e,f)|0;i=g;return (C=c[f+4>>2]|0,c[f>>2]|0)|0}function Bj(a,b,d,e,f){a=a|0;b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;l=a;j=b;k=j;h=d;n=e;i=n;if(!k){g=(f|0)!=0;if(!i){if(g){c[f>>2]=(l>>>0)%(h>>>0);c[f+4>>2]=0}n=0;f=(l>>>0)/(h>>>0)>>>0;return (C=n,f)|0}else{if(!g){n=0;f=0;return (C=n,f)|0}c[f>>2]=a|0;c[f+4>>2]=b&0;n=0;f=0;return (C=n,f)|0}}g=(i|0)==0;do if(h){if(!g){g=(aa(i|0)|0)-(aa(k|0)|0)|0;if(g>>>0<=31){m=g+1|0;i=31-g|0;b=g-31>>31;h=m;a=l>>>(m>>>0)&b|k<>>(m>>>0)&b;g=0;i=l<>2]=a|0;c[f+4>>2]=j|b&0;n=0;f=0;return (C=n,f)|0}g=h-1|0;if(g&h|0){i=(aa(h|0)|0)+33-(aa(k|0)|0)|0;p=64-i|0;m=32-i|0;j=m>>31;o=i-32|0;b=o>>31;h=i;a=m-1>>31&k>>>(o>>>0)|(k< >>(i>>>0))&b;b=b&k>>>(i>>>0);g=l< >>(o>>>0))&j|l<
>31;break}if(f|0){c[f>>2]=g&l;c[f+4>>2]=0}if((h|0)==1){o=j|b&0;p=a|0|0;return (C=o,p)|0}else{p=uj(h|0)|0;o=k>>>(p>>>0)|0;p=k<<32-p|l>>>(p>>>0)|0;return (C=o,p)|0}}else{if(g){if(f|0){c[f>>2]=(k>>>0)%(h>>>0);c[f+4>>2]=0}o=0;p=(k>>>0)/(h>>>0)>>>0;return (C=o,p)|0}if(!l){if(f|0){c[f>>2]=0;c[f+4>>2]=(k>>>0)%(i>>>0)}o=0;p=(k>>>0)/(i>>>0)>>>0;return (C=o,p)|0}g=i-1|0;if(!(g&i)){if(f|0){c[f>>2]=a|0;c[f+4>>2]=g&k|b&0}o=0;p=k>>>((uj(i|0)|0)>>>0);return (C=o,p)|0}g=(aa(i|0)|0)-(aa(k|0)|0)|0;if(g>>>0<=30){b=g+1|0;i=31-g|0;h=b;a=k<>>(b>>>0);b=k>>>(b>>>0);g=0;i=l<>2]=a|0;c[f+4>>2]=j|b&0;o=0;p=0;return (C=o,p)|0}while(0);if(!h){k=i;j=0;i=0}else{m=d|0|0;l=n|e&0;k=mj(m|0,l|0,-1,-1)|0;d=C;j=i;i=0;do{e=j;j=g>>>31|j<<1;g=i|g<<1;e=a<<1|e>>>31|0;n=a>>>31|b<<1|0;rj(k|0,d|0,e|0,n|0)|0;p=C;o=p>>31|((p|0)<0?-1:0)<<1;i=o&1;a=rj(e|0,n|0,o&m|0,(((p|0)<0?-1:0)>>31|((p|0)<0?-1:0)<<1)&l|0)|0;b=C;h=h-1|0}while((h|0)!=0);k=j;j=0}h=0;if(f|0){c[f>>2]=a;c[f+4>>2]=b}o=(g|0)>>>31|(k|h)<<1|(h<<1|g>>>31)&0|j;p=(g<<1|0>>>31)&-2|i;return (C=o,p)|0}function Cj(a,b,c,d,e,f,g,h){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;g=g|0;h=h|0;qa[a&3](b|0,c|0,d|0,e|0,f|0,g|0,h|0)}function Dj(a,b,c,d,e,f,g){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;g=g|0;ba(0)} + + // EMSCRIPTEN_END_FUNCS + var qa=[Dj,yi,xi,Dj];return{_opus_decoder_get_size:ai,_opus_get_version_string:gb,_free:kj,_opus_encode_float:Ri,_opus_strerror:fb,_i64Add:mj,_memmove:qj,_opus_decoder_init:ci,_bitshift64Ashr:nj,_opus_encoder_get_size:ti,_memset:oj,_malloc:jj,_opus_decoder_ctl:si,_opus_encode:Qi,_opus_encoder_init:vi,_opus_decode:mi,_opus_packet_get_nb_samples:oi,_memcpy:pj,_opus_encoder_ctl:Si,_opus_decode_float:ri,runPostSets:lj,stackAlloc:ra,stackSave:sa,stackRestore:ta,establishStackSpace:ua,setThrew:va,setTempRet0:ya,getTempRet0:za,dynCall_viiiiiii:Cj}}) + + + // EMSCRIPTEN_END_ASM + (b.s,b.t,buffer);b._opus_decoder_get_size=Z._opus_decoder_get_size;b._opus_get_version_string=Z._opus_get_version_string;var va=b._free=Z._free; + b._opus_encode_float=Z._opus_encode_float;b._opus_strerror=Z._opus_strerror;var Pa=b._i64Add=Z._i64Add,Ua=b._memmove=Z._memmove;b._opus_decoder_init=Z._opus_decoder_init;var Qa=b._bitshift64Ashr=Z._bitshift64Ashr;b._opus_encoder_get_size=Z._opus_encoder_get_size;var Ra=b._memset=Z._memset,Q=b._malloc=Z._malloc;b._opus_packet_get_nb_samples=Z._opus_packet_get_nb_samples;b._opus_encode=Z._opus_encode;b._opus_encoder_init=Z._opus_encoder_init;b._opus_decode=Z._opus_decode;b._opus_decoder_ctl=Z._opus_decoder_ctl; + var Sa=b._memcpy=Z._memcpy;b._opus_encoder_ctl=Z._opus_encoder_ctl;b._opus_decode_float=Z._opus_decode_float;b.runPostSets=Z.runPostSets;b.dynCall_viiiiiii=Z.dynCall_viiiiiii;y.f=Z.stackAlloc;y.g=Z.stackSave;y.c=Z.stackRestore;y.I=Z.establishStackSpace;y.B=Z.setTempRet0;y.w=Z.getTempRet0;function w(a){this.name="ExitStatus";this.message="Program terminated with exit("+a+")";this.status=a}w.prototype=Error();w.prototype.constructor=w;var Wa=null,X=function Xa(){b.calledRun||Ya();b.calledRun||(X=Xa)}; + b.callMain=b.G=function(a){function c(){for(var a=0;3>a;a++)e.push(0)}a=a||[];T||(T=!0,V(Ea));var d=a.length+1,e=[O(Ka(b.thisProgram),"i8",0)];c();for(var g=0;g > 1) + i] = pcmData[i]; + } + + // 为Opus编码数据分配内存 + const maxEncodedSize = this.maxPacketSize; + const encodedPtr = mod._malloc(maxEncodedSize); + + // 编码 + const encodedBytes = mod._opus_encode( + this.encoderPtr, + pcmPtr, + this.frameSize, + encodedPtr, + maxEncodedSize + ); + + if (encodedBytes < 0) { + mod._free(pcmPtr); + mod._free(encodedPtr); + throw new Error(`Opus编码失败: ${encodedBytes}`); + } + + // 复制编码后的数据 + const encodedData = new Uint8Array(encodedBytes); + for (let i = 0; i < encodedBytes; i++) { + encodedData[i] = mod.HEAPU8[encodedPtr + i]; + } + + // 释放内存 + mod._free(pcmPtr); + mod._free(encodedPtr); + + return encodedData; + }, + + // 销毁方法 + destroy: function() { + if (this.encoderPtr) { + this.module._free(this.encoderPtr); + this.encoderPtr = null; + } + } + }; + + // 创建解码器 + opusDecoder = { + channels: CHANNELS, + rate: SAMPLE_RATE, + frameSize: FRAME_SIZE, + module: mod, + + // 初始化解码器 + init: function() { + // 获取解码器大小 + const decoderSize = mod._opus_decoder_get_size(this.channels); + console.log(`Opus解码器大小: ${decoderSize}字节`); + + // 分配内存 + 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) { + throw new Error(`Opus解码器初始化失败: ${err}`); + } + + return true; + }, + + // 解码方法 + decode: function(opusData) { + 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; + }, + + // 销毁方法 + destroy: function() { + if (this.decoderPtr) { + this.module._free(this.decoderPtr); + this.decoderPtr = null; + } + } + }; + + // 初始化编码器和解码器 + if (opusEncoder.init() && opusDecoder.init()) { + console.log("Opus 编码器和解码器初始化成功。"); + return true; + } else { + console.error("Opus 初始化失败"); + return false; + } + } catch (error) { + console.error("Opus 初始化失败:", error); + return false; + } +} + +// 将Float32音频数据转换为Int16音频数据 +function convertFloat32ToInt16(float32Data) { + const int16Data = new Int16Array(float32Data.length); + for (let i = 0; i < float32Data.length; i++) { + // 将[-1,1]范围转换为[-32768,32767] + const s = Math.max(-1, Math.min(1, float32Data[i])); + int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + return int16Data; +} + +// 将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; +} + +function startRecording() { + if (isRecording) return; + + // 确保有权限并且AudioContext是活跃的 + if (audioContext.state === 'suspended') { + audioContext.resume().then(() => { + console.log("AudioContext已恢复"); + continueStartRecording(); + }).catch(err => { + console.error("恢复AudioContext失败:", err); + statusLabel.textContent = "无法激活音频上下文,请再次点击"; + }); + } else { + continueStartRecording(); + } +} + +// 实际开始录音的逻辑 +function continueStartRecording() { + // 重置录音数据 + recordedPcmData = []; + recordedOpusData = []; + window.audioDataBuffer = new Int16Array(0); // 重置缓冲区 + + // 初始化Opus + initOpus().then(success => { + if (!success) { + statusLabel.textContent = "Opus初始化失败"; + return; + } + + console.log("开始录音,参数:", { + sampleRate: SAMPLE_RATE, + channels: CHANNELS, + frameSize: FRAME_SIZE, + bufferSize: BUFFER_SIZE + }); + + // 请求麦克风权限 + navigator.mediaDevices.getUserMedia({ + audio: { + sampleRate: SAMPLE_RATE, + channelCount: CHANNELS, + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true + } + }) + .then(stream => { + console.log("获取到麦克风流,实际参数:", stream.getAudioTracks()[0].getSettings()); + + // 检查流是否有效 + if (!stream || !stream.getAudioTracks().length || !stream.getAudioTracks()[0].enabled) { + throw new Error("获取到的音频流无效"); + } + + mediaStream = stream; + mediaSource = audioContext.createMediaStreamSource(stream); + + // 创建ScriptProcessor(虽然已弃用,但兼容性好) + // 在降级到ScriptProcessor之前尝试使用AudioWorklet + createAudioProcessor().then(processor => { + if (processor) { + console.log("使用AudioWorklet处理音频"); + audioProcessor = processor; + // 连接音频处理链 + mediaSource.connect(audioProcessor); + audioProcessor.connect(audioContext.destination); + } else { + console.log("回退到ScriptProcessor"); + // 创建ScriptProcessor节点 + audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, CHANNELS, CHANNELS); + + // 处理音频数据 + audioProcessor.onaudioprocess = processAudioData; + + // 连接音频处理链 + mediaSource.connect(audioProcessor); + audioProcessor.connect(audioContext.destination); + } + + // 更新UI + isRecording = true; + statusLabel.textContent = "录音中..."; + startButton.disabled = true; + stopButton.disabled = false; + playButton.disabled = true; + }).catch(error => { + console.error("创建音频处理器失败:", error); + statusLabel.textContent = "创建音频处理器失败"; + }); + }) + .catch(error => { + console.error("获取麦克风失败:", error); + statusLabel.textContent = "获取麦克风失败: " + error.message; + }); + }); +} + +// 创建AudioWorklet处理器 +async function createAudioProcessor() { + try { + // 尝试使用更现代的AudioWorklet API + if ('AudioWorklet' in window && 'AudioWorkletNode' in window) { + // 定义AudioWorklet处理器代码 + const workletCode = ` + class OpusRecorderProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.buffers = []; + this.frameSize = ${FRAME_SIZE}; + this.buffer = new Float32Array(this.frameSize); + this.bufferIndex = 0; + this.isRecording = false; + + this.port.onmessage = (event) => { + if (event.data.command === 'start') { + this.isRecording = true; + } else if (event.data.command === 'stop') { + this.isRecording = false; + // 发送最后的缓冲区 + if (this.bufferIndex > 0) { + const finalBuffer = this.buffer.slice(0, this.bufferIndex); + this.port.postMessage({ buffer: finalBuffer }); + } + } + }; + } + + process(inputs, outputs) { + if (!this.isRecording) return true; + + // 获取输入数据 + const input = inputs[0][0]; // mono channel + if (!input || input.length === 0) return true; + + // 将输入数据添加到缓冲区 + for (let i = 0; i < input.length; i++) { + this.buffer[this.bufferIndex++] = input[i]; + + // 当缓冲区填满时,发送给主线程 + if (this.bufferIndex >= this.frameSize) { + this.port.postMessage({ buffer: this.buffer.slice() }); + this.bufferIndex = 0; + } + } + + return true; + } + } + + registerProcessor('opus-recorder-processor', OpusRecorderProcessor); + `; + + // 创建Blob URL + const blob = new Blob([workletCode], { type: 'application/javascript' }); + const url = URL.createObjectURL(blob); + + // 加载AudioWorklet模块 + await audioContext.audioWorklet.addModule(url); + + // 创建AudioWorkletNode + const workletNode = new AudioWorkletNode(audioContext, 'opus-recorder-processor'); + + // 处理从AudioWorklet接收的消息 + workletNode.port.onmessage = (event) => { + if (event.data.buffer) { + // 使用与ScriptProcessor相同的处理逻辑 + processAudioData({ + inputBuffer: { + getChannelData: () => event.data.buffer + } + }); + } + }; + + // 启动录音 + workletNode.port.postMessage({ command: 'start' }); + + // 保存停止函数 + workletNode.stopRecording = () => { + workletNode.port.postMessage({ command: 'stop' }); + }; + + console.log("AudioWorklet 音频处理器创建成功"); + return workletNode; + } + } catch (error) { + console.error("创建AudioWorklet失败,将使用ScriptProcessor:", error); + } + + // 如果AudioWorklet不可用或失败,返回null以便回退到ScriptProcessor + return null; +} + +// 处理音频数据 +function processAudioData(e) { + // 获取输入缓冲区 + const inputBuffer = e.inputBuffer; + + // 获取第一个通道的Float32数据 + const inputData = inputBuffer.getChannelData(0); + + // 添加调试信息 + const nonZeroCount = Array.from(inputData).filter(x => Math.abs(x) > 0.001).length; + console.log(`接收到音频数据: ${inputData.length} 个样本, 非零样本数: ${nonZeroCount}`); + + // 如果全是0,可能是麦克风没有正确获取声音 + if (nonZeroCount < 5) { + console.warn("警告: 检测到大量静音样本,请检查麦克风是否正常工作"); + // 继续处理,以防有些样本确实是静音 + } + + // 存储PCM数据用于调试 + recordedPcmData.push(new Float32Array(inputData)); + + // 转换为Int16数据供Opus编码 + const int16Data = convertFloat32ToInt16(inputData); + + // 如果收集到的数据不是FRAME_SIZE的整数倍,需要进行处理 + // 创建静态缓冲区来存储不足一帧的数据 + if (!window.audioDataBuffer) { + window.audioDataBuffer = new Int16Array(0); + } + + // 合并之前缓存的数据和新数据 + const combinedData = new Int16Array(window.audioDataBuffer.length + int16Data.length); + combinedData.set(window.audioDataBuffer); + combinedData.set(int16Data, window.audioDataBuffer.length); + + // 处理完整帧 + const frameCount = Math.floor(combinedData.length / FRAME_SIZE); + console.log(`可编码的完整帧数: ${frameCount}, 缓冲区总大小: ${combinedData.length}`); + + for (let i = 0; i < frameCount; i++) { + const frameData = combinedData.subarray(i * FRAME_SIZE, (i + 1) * FRAME_SIZE); + + try { + console.log(`编码第 ${i+1}/${frameCount} 帧, 帧大小: ${frameData.length}`); + const encodedData = opusEncoder.encode(frameData); + if (encodedData) { + console.log(`编码成功: ${encodedData.length} 字节`); + recordedOpusData.push(encodedData); + } + } catch (error) { + console.error(`Opus编码帧 ${i+1} 失败:`, error); + } + } + + // 保存剩余不足一帧的数据 + const remainingSamples = combinedData.length % FRAME_SIZE; + if (remainingSamples > 0) { + window.audioDataBuffer = combinedData.subarray(frameCount * FRAME_SIZE); + console.log(`保留 ${remainingSamples} 个样本到下一次处理`); + } else { + window.audioDataBuffer = new Int16Array(0); + } +} + +function stopRecording() { + if (!isRecording) return; + + // 处理剩余的缓冲数据 + if (window.audioDataBuffer && window.audioDataBuffer.length > 0) { + console.log(`停止录音,处理剩余的 ${window.audioDataBuffer.length} 个样本`); + // 如果剩余数据不足一帧,可以通过补零的方式凑成一帧 + if (window.audioDataBuffer.length < FRAME_SIZE) { + const paddedFrame = new Int16Array(FRAME_SIZE); + paddedFrame.set(window.audioDataBuffer); + // 剩余部分填充为0 + for (let i = window.audioDataBuffer.length; i < FRAME_SIZE; i++) { + paddedFrame[i] = 0; + } + try { + console.log(`编码最后一帧(补零): ${paddedFrame.length} 样本`); + const encodedData = opusEncoder.encode(paddedFrame); + if (encodedData) { + recordedOpusData.push(encodedData); + } + } catch (error) { + console.error("最后一帧Opus编码失败:", error); + } + } else { + // 如果数据超过一帧,按正常流程处理 + processAudioData({ + inputBuffer: { + getChannelData: () => convertInt16ToFloat32(window.audioDataBuffer) + } + }); + } + window.audioDataBuffer = null; + } + + // 如果使用的是AudioWorklet,调用其特定的停止方法 + if (audioProcessor && typeof audioProcessor.stopRecording === 'function') { + audioProcessor.stopRecording(); + } + + // 停止麦克风 + if (mediaStream) { + mediaStream.getTracks().forEach(track => track.stop()); + } + + // 断开音频处理链 + if (audioProcessor) { + try { + audioProcessor.disconnect(); + if (mediaSource) mediaSource.disconnect(); + } catch (error) { + console.warn("断开音频处理链时出错:", error); + } + } + + // 更新UI + isRecording = false; + statusLabel.textContent = "已停止录音,收集了 " + recordedOpusData.length + " 帧Opus数据"; + startButton.disabled = false; + stopButton.disabled = true; + playButton.disabled = recordedOpusData.length === 0; + + console.log("录制完成:", + "PCM帧数:", recordedPcmData.length, + "Opus帧数:", recordedOpusData.length); +} + +function playRecording() { + if (!recordedOpusData.length) { + statusLabel.textContent = "没有可播放的录音"; + return; + } + + // 将所有Opus数据解码为PCM + let allDecodedData = []; + + for (const opusData of recordedOpusData) { + try { + // 解码为Int16数据 + const decodedData = opusDecoder.decode(opusData); + + if (decodedData && decodedData.length > 0) { + // 将Int16数据转换为Float32 + const float32Data = convertInt16ToFloat32(decodedData); + + // 添加到总解码数据中 + allDecodedData.push(...float32Data); + } + } catch (error) { + console.error("Opus解码失败:", error); + } + } + + // 如果没有解码出数据,返回 + if (allDecodedData.length === 0) { + statusLabel.textContent = "解码失败,无法播放"; + return; + } + + // 创建音频缓冲区 + const audioBuffer = audioContext.createBuffer(CHANNELS, allDecodedData.length, SAMPLE_RATE); + audioBuffer.copyToChannel(new Float32Array(allDecodedData), 0); + + // 创建音频源并播放 + const source = audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(audioContext.destination); + source.start(); + + // 更新UI + statusLabel.textContent = "正在播放..."; + playButton.disabled = true; + + // 播放结束后恢复UI + source.onended = () => { + statusLabel.textContent = "播放完毕"; + playButton.disabled = false; + }; +} + +// 模拟服务端返回的Opus数据进行解码播放 +function playOpusFromServer(opusData) { + // 这个函数展示如何处理服务端返回的opus数据 + // opusData应该是一个包含opus帧的数组 + + if (!opusDecoder) { + initOpus().then(success => { + if (success) { + decodeAndPlayOpusData(opusData); + } else { + statusLabel.textContent = "Opus解码器初始化失败"; + } + }); + } else { + decodeAndPlayOpusData(opusData); + } +} + +function decodeAndPlayOpusData(opusData) { + let allDecodedData = []; + + for (const frame of opusData) { + try { + const decodedData = opusDecoder.decode(frame); + if (decodedData && decodedData.length > 0) { + const float32Data = convertInt16ToFloat32(decodedData); + allDecodedData.push(...float32Data); + } + } catch (error) { + console.error("服务端Opus数据解码失败:", error); + } + } + + if (allDecodedData.length === 0) { + statusLabel.textContent = "服务端数据解码失败"; + return; + } + + const audioBuffer = audioContext.createBuffer(CHANNELS, allDecodedData.length, SAMPLE_RATE); + audioBuffer.copyToChannel(new Float32Array(allDecodedData), 0); + + const source = audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(audioContext.destination); + source.start(); + + statusLabel.textContent = "正在播放服务端数据..."; + source.onended = () => statusLabel.textContent = "服务端数据播放完毕"; +} diff --git a/vue/apps/bot_web_test/opus_test/test.html b/vue/apps/bot_web_test/opus_test/test.html new file mode 100644 index 0000000..41866ad --- /dev/null +++ b/vue/apps/bot_web_test/opus_test/test.html @@ -0,0 +1,464 @@ + + + + + Opus 编解码测试 + + + +++ + + + + + + diff --git a/vue/apps/bot_web_test/test_page.css b/vue/apps/bot_web_test/test_page.css new file mode 100644 index 0000000..bea0499 --- /dev/null +++ b/vue/apps/bot_web_test/test_page.css @@ -0,0 +1,425 @@ +body { + font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; + margin: 0; + padding: 20px; + background-color: #f5f5f5; +} + +.container { + max-width: 1000px; + margin: 0 auto; + background-color: white; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 10px 20px 10px 20px; +} + +h1 { + color: #333; + text-align: center; + margin-bottom: 30px; +} + +.section { + margin-bottom: 5px; + padding: 10px; + border-radius: 10px; + background-color: #f9f9f9; +} + +.section h2 { + margin-top: 0; + color: #444; + font-size: 18px; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 0; +} + +.section h2 .toggle-button { + margin-left: auto; + padding: 4px 12px; + font-size: 12px; + border-radius: 4px; + cursor: pointer; + height: 28px; + line-height: 20px; +} + +.device-info { + display: flex; + align-items: center; + gap: 20px; + margin-left: 20px; + padding: 0 15px; + background-color: #f9f9f9; + border-radius: 4px; + height: 28px; + line-height: 28px; +} + +.device-info span { + color: #666; + font-size: 13px; +} + +.device-info strong { + color: #333; + font-weight: 500; +} + +.config-panel { + display: none; + transition: all 0.3s ease; + margin-top: 5px; + padding: 5px 0; +} + +.config-panel.expanded { + display: block; +} + +.control-panel { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} + +button { + padding: 8px 15px; + border: none; + border-radius: 5px; + background-color: #4285f4; + color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover { + background-color: #3367d6; +} + +button:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +#serverUrl, +#otaUrl { + flex-grow: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; +} + +.message-input { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +#messageInput { + flex-grow: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; +} + +#nfcCardId { + flex-grow: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; +} + +.conversation { + max-height: 300px; + overflow-y: auto; + border: 1px solid #ddd; + border-radius: 5px; + padding: 10px; + background-color: white; + flex: 1; + margin-right: 10px; +} + +.message { + margin-bottom: 10px; + padding: 8px 12px; + border-radius: 8px; + max-width: 80%; +} + +.user { + background-color: #e2f2ff; + margin-left: auto; + margin-right: 10px; + text-align: right; +} + +.server { + background-color: #f0f0f0; + margin-right: auto; + margin-left: 10px; +} + +.status { + color: #666; + font-style: italic; + font-size: 14px; + margin: 0; + padding: 0; +} + +.audio-controls { + display: flex; + justify-content: center; + gap: 10px; + margin-top: 20px; +} + +.audio-visualizer { + height: 60px; + width: 100%; + margin-top: 10px; + border: 1px solid #ddd; + border-radius: 5px; + background-color: #fafafa; +} + +.record-button { + background-color: #db4437; +} + +.record-button:hover { + background-color: #c53929; +} + +.record-button.recording { + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { + background-color: #db4437; + } + + 50% { + background-color: #ff6659; + } + + 100% { + background-color: #db4437; + } +} + +#logContainer { + margin-top: 0; + padding: 10px; + background-color: #f0f0f0; + border-radius: 5px; + font-family: monospace; + height: 300px; + overflow-y: auto; + flex: 1; + margin-left: 10px; +} + +.log-entry { + margin: 5px 0; + font-size: 12px; +} + +.log-info { + color: #333; +} + +.log-error { + color: #db4437; +} + +.log-success { + color: #0f9d58; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translate(-50%, -60%); + } + + to { + opacity: 1; + transform: translate(-50%, -50%); + } +} + +.script-status { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 5px; +} + +.script-loaded { + background-color: #0f9d58; +} + +.script-loading { + background-color: #f4b400; +} + +.script-error { + background-color: #db4437; +} + +.script-list { + margin: 10px 0; + padding: 10px; + background-color: #f9f9f9; + border-radius: 5px; + font-family: monospace; + font-size: 11px; +} + +#scriptStatus.success { + background-color: #e6f4ea; + color: #0f9d58; + border-left: 4px solid #0f9d58; +} + +#scriptStatus.error { + background-color: #fce8e6; + color: #db4437; + border-left: 4px solid #db4437; +} + +#scriptStatus.warning { + background-color: #fef7e0; + color: #f4b400; + border-left: 4px solid #f4b400; +} + +/* 标签页样式 */ +.tabs { + display: flex; + margin-bottom: 20px; + border-bottom: 2px solid #e0e0e0; +} + +.tab { + padding: 10px 20px; + cursor: pointer; + border: none; + background: none; + font-size: 16px; + color: #666; + position: relative; + transition: all 0.3s ease; +} + +.tab:hover { + color: #4285f4; +} + +.tab.active { + color: #4285f4; + font-weight: bold; +} + +.tab.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 2px; + background-color: #4285f4; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +.flex-container { + display: flex; + gap: 20px; + margin-top: 10px; +} + +.config-item { + display: flex; + align-items: center; + margin-bottom: 8px; + width: 100%; +} + +.config-item label { + width: 100px; + text-align: right; + margin-right: 10px; + color: #666; +} + +.config-item input { + flex-grow: 1; + padding: 6px; + border: 1px solid #ddd; + border-radius: 5px; +} + +.control-panel { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 10px; +} + +.connection-controls { + display: flex; + gap: 10px; + align-items: center; + width: 100%; +} + +.connection-controls input { + flex: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; + min-width: 200px; +} + +.connection-controls button { + white-space: nowrap; + padding: 8px 15px; +} + +.connection-status { + display: flex; + align-items: center; + gap: 20px; + margin-left: 20px; + padding: 0 15px; + background-color: #f9f9f9; + border-radius: 4px; + height: 28px; + line-height: 28px; +} + +.connection-status span { + color: #666; + font-size: 13px; +} + +.connection-status .status { + color: #333; + font-weight: 500; +} \ No newline at end of file diff --git a/vue/apps/bot_web_test/test_page.html b/vue/apps/bot_web_test/test_page.html new file mode 100644 index 0000000..a6672db --- /dev/null +++ b/vue/apps/bot_web_test/test_page.html @@ -0,0 +1,2224 @@ + + + + + + +Opus 编解码录音播放测试
+ +正在加载Opus库...+ ++ + + + + + ++录音状态: 待机,正在初始化...
+ + + +小智服务器测试页面 + + + + + + +++ + + + + + + + \ No newline at end of file小智服务器测试页面
+ ++ 正在加载Opus库... ++ + +++ ++ 设备配置 + + MAC: + 客户端: web_test_client + + +
++++++ + +++ + +++ + +++ + ++++ ++ 连接信息 + + OTA: ota未连接 + WS: ws未连接 + +
++ + + ++++ ++ + ++ ++ ++ ++++ ++ +++ + +会话记录
++ ++++准备就绪,请连接服务器开始测试...+++ + + ++ MCP 工具管理 + + 0 个工具 + + +
++++ +++ ++