260 lines
6.1 KiB
Markdown
260 lines
6.1 KiB
Markdown
|
|
# 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 回调未触发,监听器设置位置不当
|
|||
|
|
**解决方案:** 将监听器设置移到初始化阶段,只设置一次
|
|||
|
|
**状态:** ✅ 已修复,待测试
|