diff --git a/public/audio-processor.js b/public/audio-processor.js new file mode 100644 index 0000000..f7f07b4 --- /dev/null +++ b/public/audio-processor.js @@ -0,0 +1,40 @@ +class AudioProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.desiredBufferSize = 20*1024; // 期望的缓冲区大小 + this.buffer = new Float32Array(this.desiredBufferSize); + this.bufferIndex = 0; + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const channel = input[0]; + + if (channel && channel.length > 0) { + // 将新数据添加到缓冲区 + for (let i = 0; i < channel.length; i++) { + this.buffer[this.bufferIndex++] = channel[i]; + + // 当缓冲区满时,发送数据 + if (this.bufferIndex >= this.desiredBufferSize) { + // 将浮点音频数据转换为16位整数 + const pcmData = new Int16Array(this.desiredBufferSize); + for (let j = 0; j < this.desiredBufferSize; j++) { + pcmData[j] = Math.min(Math.max(this.buffer[j] * 32767, -32767), 32767); + } + + // 发送数据到主线程 + this.port.postMessage(pcmData.buffer, [pcmData.buffer]); + + // 重置缓冲区 + this.buffer = new Float32Array(this.desiredBufferSize); + this.bufferIndex = 0; + } + } + } + + return true; + } +} + +registerProcessor('audio-processor', AudioProcessor); \ No newline at end of file diff --git a/src/assets/js/GamepadController.js b/src/assets/js/GamepadController.js index 942f5e4..d283ad2 100644 --- a/src/assets/js/GamepadController.js +++ b/src/assets/js/GamepadController.js @@ -1,84 +1,137 @@ class GamepadController { - constructor(config = {}) { - this.config = { - buttonMapping: {}, - axisMapping: {}, - ...config + constructor(options = {}) { + this.options = { + ...{ + debug: false, + deadZone: 0.1, + updateInterval: 50, + buttonsConfig: [ + { name: "Left2", index: 6 }, + { name: "Back", index: 8 }, + { name: "Right Joystick Press", index: 11 } + ] + }, + ...options }; + + this.gamepadIndex = null; this.gamepad = null; - this.listeners = {}; + this.interval = null; + this.buttons = []; + this.directionAxis0_1 = ""; + this.directionAxis9 = ""; + this.angle = 0; + // 初始化按钮状态 + this.buttons = this.options.buttonsConfig.map(button => ({ + ...button, + pressed: false + })); - // 初始化默认按键映射 - Object.assign(this.config.buttonMapping, { - 0: 'A', - 1: 'B', - 2: 'X', - 3: 'Y', - // 其他默认映射 - }); + // 注册事件监听器 + window.addEventListener("gamepadconnected", this.onGamepadConnected.bind(this)); + window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected.bind(this)); - // 初始化默认摇杆映射 - Object.assign(this.config.axisMapping, { - 0: 'leftX', - 1: 'leftY', - 2: 'rightX', - 3: 'rightY', - // 其他默认映射 - }); + if (this.options.debug) { + console.log("GamepadController initialized with options:", this.options); + } } - connect() { - window.addEventListener('gamepadconnected', (e) => { - this.gamepad = navigator.getGamepads()[e.gamepad.index]; - this.triggerEvent('connected'); - }); + onGamepadConnected(e) { + console.log("Gamepad connected:", e.gamepad); + this.gamepadIndex = e.gamepad.index; + this.gamepad = navigator.getGamepads()[this.gamepadIndex]; + this.startGamepad(); } - disconnect() { - window.removeEventListener('gamepadconnected', this.handleConnect); + onGamepadDisconnected() { + clearInterval(this.interval); this.gamepad = null; - this.triggerEvent('disconnected'); + if (this.options.debug) { + console.log("Gamepad disconnected"); + } } - // 手动获取手柄数据的方法 - pollGamepad() { - if (!this.gamepad) return; - - // 处理摇杆数据 - const axesData = this.gamepad.axes; - const axisEvents = {}; - Object.entries(this.config.axisMapping).forEach(([index, name]) => { - axisEvents[name] = axesData[index]; - }); - this.triggerEvent('axisChange', axisEvents); - - // 处理按键事件 - const buttons = this.gamepad.buttons; - buttons.forEach((btn, index) => { - if (btn.value === 1 && this.config.buttonMapping[index]) { - const keyName = this.config.buttonMapping[index]; - this.triggerEvent('buttonPress', { keyName, index }); + startGamepad() { + this.interval = setInterval(() => { + const gamepads = navigator.getGamepads(); + const gamepad = gamepads[this.gamepadIndex]; + + if (gamepad) { + // 注释掉调试打印 + // if (this.options.debug) { + // console.log('Axes data:', { + // axis0: gamepad.axes[0], + // axis1: gamepad.axes[1], + // gamepadIndex: this.gamepadIndex + // }); + // } + + this.updateDirection(gamepad.axes); + this.updateDirectionAxis9(gamepad.axes); + this.pressKey(gamepad.buttons); } + }, this.options.updateInterval); + } + + updateDirection(axes) { + const axis0 = axes[0]; + const axis1 = axes[1]; + + // 检查是否在死区 + if (Math.abs(axis0) < this.options.deadZone && Math.abs(axis1) < this.options.deadZone) { + this.directionAxis0_1 = "未定义"; + this.angle = 0; // 在死区时重置角度为0 + return; + } + + // 计算方向角度(0-360度) + let angle = Math.atan2(axis1, axis0) * (180 / Math.PI); + angle = (angle + 360) % 360; // 确保角度在 0-360 范围内 + angle = Math.round(angle); + this.angle = angle; + + // 更新方向数据 + if (Math.abs(axis0) > this.options.deadZone || Math.abs(axis1) > this.options.deadZone) { + this.directionAxis0_1 = `${angle}°`; + // 注释掉调试打印 + // if (this.options.debug) { + // console.log(` 摇杆方向: ${angle}°, X轴: ${axis0.toFixed(2)}, Y轴: ${axis1.toFixed(2)}`); + // } + } + } + + updateDirectionAxis9(axes) { + const axis9 = axes[9]; + const roundedAxis9 = Math.round(axis9 * 100) / 100; + if (roundedAxis9 <= -0.9) { + this.directionAxis9 = "上"; + } else if (roundedAxis9 >= 0.0 && roundedAxis9 <= 0.2) { + this.directionAxis9 = "下"; + } else if (roundedAxis9 >= 0.6 && roundedAxis9 <= 0.8) { + this.directionAxis9 = "左"; + } else if (roundedAxis9 >= -0.5 && roundedAxis9 <= -0.4) { + this.directionAxis9 = "右"; + } else { + this.directionAxis9 = "未定义"; + } + } + + pressKey(buttons) { + this.buttons.forEach(button => { + const buttonData = buttons[button.index]; + button.pressed = buttonData ? buttonData.value === 1 : false; }); } - addEventListener(type, callback) { - if (!this.listeners[type]) this.listeners[type] = []; - this.listeners[type].push(callback); - } - - triggerEvent(type, data = {}) { - if (this.listeners[type]) { - this.listeners[type].forEach(callback => callback(data)); + destroy() { + clearInterval(this.interval); + window.removeEventListener("gamepadconnected", this.onGamepadConnected); + window.removeEventListener("gamepaddisconnected", this.onGamepadDisconnected); + if (this.options.debug) { + console.log("GamepadController destroyed"); } } } -// 导出模块 -if (typeof module !== 'undefined' && module.exports) { - module.exports = GamepadController; -} else if (typeof define === 'function' && define.amd) { - define([], () => GamepadController); -} else { - window.GamepadController = GamepadController; -} \ No newline at end of file +// 导出类以便外部使用 +export default GamepadController; \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 7efe543..58945eb 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,7 +1,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'; import { useStore } from 'vuex'; import Home from '../views/home/home.vue'; -import CarControl from '../views/CarControl.vue'; // 导入小车控制页面 +import CarControl from '../views/CarControl.vue'; // 直接导入组件 const routes = [ { @@ -48,9 +48,9 @@ const routes = [ component: () => import('../views/voice/voiceset.vue') }, { - path: '/car_control', // 添加小车控制的路由 + path: '/car_control', name: 'CarControl', - component: () => import('../views/CarControl.vue') + component: CarControl, // 直接使用导入的组件,而不是使用动态导入 }, { path: '/audio-play', diff --git a/src/views/AudioPlay.vue b/src/views/AudioPlay.vue index a034644..c447bb6 100644 --- a/src/views/AudioPlay.vue +++ b/src/views/AudioPlay.vue @@ -5,6 +5,31 @@ {{ isRecording ? '停止传输音频' : '开始传输音频' }}

{{ status }}

+
+
+ + +
+
+ + +
+
+ + +
+
@@ -16,67 +41,109 @@ export default { mediaRecorder: null, // MediaRecorder 实例 audioChunks: [], // 音频数据 status: '点击按钮开始传输音频', - isRecording: false // 用于判断是否在录音 + isRecording: false, // 用于判断是否在录音 + audioConfig: { + sampleRate: 16000, + bitsPerSample: 16, + channels: 1 + }, + stream: null, + audioContext: null, + worklet: null, + // lastSendTime: 0, + // sendInterval: 200, // 修改为 500ms }; }, mounted() { // 初始化 WebSocket 连接 - this.ws = new WebSocket('ws://192.168.1.60:81'); // 替换为 ESP32 IP 地址 - this.ws.onopen = () => { - console.log('WebSocket连接已打开'); + this.ws = new WebSocket('ws://192.168.4.103/ws'); // 替换为 ESP32 IP 地址 + // this.ws = new WebSocket('ws://192.168.1.60:81'); // 替换为您的 ESP32 IP + + this.ws.onopen = () => { + console.log('WebSocket 连接已打开'); }; - this.ws.onmessage = (event) => { - console.log('接收到来自ESP32的消息:', event.data); + this.ws.onmessage = (event) => { + console.log(' 接收到来自ESP32的消息:', event.data); }; - this.ws.onclose = () => { - console.log('WebSocket连接已关闭'); + this.ws.onclose = () => { + console.log('WebSocket 连接已关闭'); }; }, methods: { toggleRecording() { - if (this.isRecording) { + if (this.isRecording) { // 停止录音 - this.stopRecording(); + this.stopRecording(); } else { // 开始录音 - this.startRecording(); + this.startRecording(); } }, - startRecording() { + async startRecording() { this.status = '开始录音并传输音频...'; - this.audioChunks = []; this.isRecording = true; - navigator.mediaDevices.getUserMedia({ audio: true }) - .then((stream) => { - this.mediaRecorder = new MediaRecorder(stream); - this.mediaRecorder.ondataavailable = (event) => { - this.audioChunks.push(event.data); - // 每次接收到音频数据时实时发送 - const audioBlob = new Blob([event.data], { type: 'audio/wav' }); - audioBlob.arrayBuffer().then((arrayBuffer) => { - // 打印音频数据 - console.log('实时音频数据:', new Uint8Array(arrayBuffer)); - // 发送音频数据到 WebSocket - if (this.ws.readyState === WebSocket.OPEN) { - this.ws.send(arrayBuffer); - } - }); - }; - this.mediaRecorder.start(100); // 每100ms捕获一次音频数据 - }) - .catch((err) => { - console.error('无法访问麦克风:', err); + const constraints = { + audio: { + sampleRate: parseInt(this.audioConfig.sampleRate), + channelCount: parseInt(this.audioConfig.channels), + sampleSize: parseInt(this.audioConfig.bitsPerSample) + } + }; + + try { + const stream = await navigator.mediaDevices.getUserMedia(constraints); + + // 创建 AudioContext + const audioContext = new AudioContext({ + sampleRate: parseInt(this.audioConfig.sampleRate) }); + + // 加载 AudioWorklet + await audioContext.audioWorklet.addModule('audio-processor.js'); + + const source = audioContext.createMediaStreamSource(stream); + const worklet = new AudioWorkletNode(audioContext, 'audio-processor'); + + // 监听来自 worklet 的消息 + worklet.port.onmessage = (event) => { + if (this.ws.readyState === WebSocket.OPEN) { + // const currentTime = Date.now(); + // if (!this.lastSendTime || currentTime - this.lastSendTime > this.sendInterval) { + const pcmData = new Int16Array(event.data); + // 只打印原始数据 + console.log('发送的音频数据:', Array.from(pcmData.slice(0, 20))); // 显示前20个采样点 + this.ws.send(event.data); + // this.lastSendTime = currentTime; + // } + } + }; + source.connect(worklet); + worklet.connect(audioContext.destination); + + // 保存引用以便后续停止 + this.stream = stream; + this.audioContext = audioContext; + this.worklet = worklet; + } catch (err) { + console.error('无法访问麦克风:', err); + this.status = '访问麦克风失败: ' + err.message; + } }, stopRecording() { this.status = '停止录音'; this.isRecording = false; - if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') { - this.mediaRecorder.stop(); // 停止录音并发送数据 + if (this.stream) { + this.stream.getTracks().forEach(track => track.stop()); + } + if (this.worklet) { + this.worklet.disconnect(); + } + if (this.audioContext) { + this.audioContext.close(); } } } @@ -89,4 +156,24 @@ button { font-size: 16px; cursor: pointer; } - + +.audio-settings { + margin: 20px 0; + padding: 15px; + border: 1px solid #ddd; + border-radius: 5px; +} + +.setting-item { + margin: 10px 0; +} + +.setting-item label { + margin-right: 10px; +} + +select { + padding: 5px; + border-radius: 3px; +} + \ No newline at end of file diff --git a/src/views/CarControl.vue b/src/views/CarControl.vue index 2c2a56c..44ef0a3 100644 --- a/src/views/CarControl.vue +++ b/src/views/CarControl.vue @@ -6,12 +6,13 @@
遥控车当前状态 - voice + voice
- link - mp3 + link + battery
@@ -33,7 +34,7 @@
避障 - +
@@ -41,7 +42,11 @@
云台状态
- car +
+ boom + car + boom +
@@ -50,15 +55,22 @@
- refresh + refresh
- play + play
- control +
+ control +
@@ -66,6 +78,9 @@ - + \ No newline at end of file diff --git a/src/views/home/home.vue b/src/views/home/home.vue index a6b45e4..06edb9e 100644 --- a/src/views/home/home.vue +++ b/src/views/home/home.vue @@ -16,6 +16,8 @@ + + diff --git a/遥控四轮车通信协议v0.3.0.250218_alpha.docx b/遥控四轮车通信协议v0.3.0.250218_alpha.docx new file mode 100644 index 0000000..35bbcbb Binary files /dev/null and b/遥控四轮车通信协议v0.3.0.250218_alpha.docx differ