# NO_VALID_AUDIO_ERROR 最终修复说明 ## 🎯 问题根源 经过详细分析日志,发现问题的根本原因是: 1. ✅ `ptt_on` 信号已发送并被服务器接收(日志显示 `ptt_enabled`) 2. ✅ 录音已启动 3. ❌ **但是 `onStop` 回调没有被触发** 4. ❌ 导致录音停止后,没有读取和发送音频文件 5. ❌ 服务器等待音频数据超时,报错 `NO_VALID_AUDIO_ERROR` ## 🔧 最终修复方案 ### 修复内容 将录音监听器的设置从 `startRecording` 方法中移出,改为在初始化时设置一次。 **原因:** - 每次调用 `startRecording` 都会重新设置监听器 - 这可能导致监听器被覆盖或 `this` 上下文丢失 - 导致 `onStop` 回调无法正常触发 ### 代码变更 #### 1. 在 `onLoad` 中调用 `setupRecorderListeners()` ```javascript onLoad() { // 初始化 recorderManager recorderManager = uni.getRecorderManager() // 设置录音监听器(只设置一次) this.setupRecorderListeners() this.getCallDuration() this.initAudio() } ``` #### 2. 新增 `setupRecorderListeners()` 方法 ```javascript setupRecorderListeners() { // 监听录音开始 recorderManager.onStart(() => { console.log('✅ 录音已开始') }) // 监听录音错误 recorderManager.onError((err) => { console.error('❌ 录音错误:', err) this.isRecording = false }) // 监听录音停止 - 读取文件并发送 recorderManager.onStop((res) => { console.log('⏹️ 录音已停止') // ... 读取文件并发送的逻辑 }) // 监听音频帧 - 实时发送(如果支持) recorderManager.onFrameRecorded((res) => { // ... 实时发送音频帧的逻辑 }) } ``` #### 3. 简化 `startRecording()` 方法 ```javascript async startRecording() { if (this.isRecording) return this.isRecording = true // 直接启动录音,不再设置监听器 const recorderOptions = { duration: 600000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: 'pcm', frameSize: 5, // 启用实时音频帧 audioSource: 'auto' } recorderManager.start(recorderOptions) } ``` ## 📊 预期效果 修复后的完整流程: ``` 1. 用户按住"按住说话" ↓ 2. 发送 ptt_on 信号 ✅ ↓ 3. 服务器响应 ptt_enabled ✅ ↓ 4. 启动录音 ✅ ↓ 5. 实时发送音频帧(如果支持) 或 用户松开按钮 ↓ 6. 停止录音,触发 onStop 回调 ← 修复重点 ↓ 7. 读取录音文件(ArrayBuffer) ↓ 8. 分片发送音频数据 ↓ 9. 发送 ptt_off 信号 ↓ 10. 服务器 ASR 识别 → LLM 生成 → TTS 合成 ``` ## 🧪 测试步骤 ### 1. 重新编译 在 HBuilderX 中: 1. 停止当前运行 2. 清理缓存(菜单 -> 运行 -> 清理缓存) 3. 重新运行到手机/模拟器 ### 2. 测试并观察日志 按住说话 3-5 秒,观察日志: **预期日志(成功):** ``` ✅ 开始说话, isTalking 设置为: true 📤 发送 ptt_on 信号 ✅ ptt_on 信号发送成功 📋 收到服务器消息: {"type":"info","msg":"ptt_enabled"} ← 服务器确认 录音未启动,开始启动录音 === startRecording 被调用 === 🎙️ 启动 recorderManager ✅ 录音已开始 [用户松开按钮] === stopTalking 被调用 === 🛑 停止录音并准备发送... ⏹️ 录音已停止 ← 关键!这个日志必须出现 📁 文件路径: /xxx/recorder/xxx.pcm ✅ 文件读取成功 📊 是否为 ArrayBuffer: true 📊 文件大小: 160000 bytes 📦 开始分片发送 📤 发送第 1 片,大小: 3200 bytes ✅ 第 1 片发送成功 ... ✅ 所有音频片段发送完成 ✅ ptt_off 信号发送成功 ``` ### 3. 关键检查点 - ✅ 必须看到 "⏹️ 录音已停止" 日志 - ✅ 必须看到 "✅ 文件读取成功" 日志 - ✅ 必须看到 "📤 发送第 X 片" 日志 - ✅ 服务器不再报 `NO_VALID_AUDIO_ERROR` ## 🐛 如果还有问题 ### 问题 1:还是没有 "⏹️ 录音已停止" 日志 **可能原因:** - 录音时间太短(< 500ms) - 录音权限未授予 - 设备不支持 PCM 格式 **解决方案:** 1. 确保录音至少 2-3 秒 2. 检查 App 权限设置,确保麦克风权限已授予 3. 尝试修改录音格式为 `aac`(但需要服务器支持) ### 问题 2:有 "⏹️ 录音已停止" 但没有文件路径 **可能原因:** - 录音失败,没有生成文件 - 存储权限问题 **解决方案:** 1. 检查日志中的录音错误信息 2. 确保 App 有存储权限 3. 检查设备存储空间是否充足 ### 问题 3:文件读取失败 **可能原因:** - 文件路径无效 - 文件系统权限问题 **解决方案:** 1. 检查 `res.tempFilePath` 的值 2. 尝试使用绝对路径 3. 检查文件是否真实存在 ## 📝 技术要点总结 ### 1. 监听器设置的最佳实践 ❌ **错误做法:** ```javascript startRecording() { // 每次都设置监听器 recorderManager.onStop(() => { ... }) recorderManager.start() } ``` ✅ **正确做法:** ```javascript onLoad() { // 初始化时设置一次 this.setupRecorderListeners() } startRecording() { // 只启动录音 recorderManager.start() } ``` ### 2. this 上下文问题 在回调函数中使用箭头函数确保 `this` 指向组件实例: ```javascript recorderManager.onStop((res) => { // 箭头函数,this 指向组件 this.socketTask.send(...) }) ``` ### 3. 双重保障机制 - **主方案:** `onFrameRecorded` 实时发送音频帧(低延迟) - **备用方案:** `onStop` 发送完整文件(兼容性好) 大多数设备会使用备用方案,因为 `onFrameRecorded` 支持有限。 ## ✅ 修复完成 所有代码修改已完成,请重新编译测试! --- **修复时间:** 2026-03-02 **问题:** NO_VALID_AUDIO_ERROR **根本原因:** onStop 回调未触发,监听器设置位置不当 **解决方案:** 将监听器设置移到初始化阶段,只设置一次 **状态:** ✅ 已修复,待测试