327 lines
11 KiB
Markdown
327 lines
11 KiB
Markdown
# AEC语音打断功能移植方案:Airhub_Rtc_h → Kapi_Rtc
|
||
|
||
## 1. 可行性总结
|
||
|
||
经过全面分析,在不启用ADF架构的情况下,将Airhub_Rtc_h项目中基于双麦克风和ES7210实现的AEC语音打断功能移植到Kapi_Rtc项目是**技术可行的**。主要依据如下:
|
||
|
||
- Kapi_Rtc项目已集成ESP-SR组件,提供了独立于ADF的AEC和VAD API
|
||
- 两个项目均支持ESP32_S3_KORVO2_V3开发板,硬件兼容性良好
|
||
- Kapi_Rtc已有AudioProcessor类可扩展以支持AEC功能
|
||
- 两个项目都支持ES7210编解码器,配置方式可兼容
|
||
|
||
## 2. 移植实现方案
|
||
|
||
### 2.1 ES7210双麦克风配置移植
|
||
|
||
1. **创建ES7210配置初始化模块**:
|
||
```cpp
|
||
// 在Kapi_Rtc中创建es7210_mic_config.h/cpp文件
|
||
#include "es7210_adc.h"
|
||
|
||
#define ES7210_MIC_COMBO_MIC1_MIC3 (ES7210_INPUT_MIC1 | ES7210_INPUT_MIC3) // 双麦克风配置
|
||
|
||
esp_err_t init_es7210_with_dual_mic(const audio_codec_ctrl_if_t *ctrl_if) {
|
||
es7210_codec_cfg_t es7210_cfg = {
|
||
.ctrl_if = ctrl_if,
|
||
.master_mode = false,
|
||
.mic_selected = ES7210_MIC_COMBO_MIC1_MIC3,
|
||
.mclk_src = ES7210_MCLK_FROM_PAD,
|
||
.mclk_div = 256
|
||
};
|
||
|
||
const audio_codec_if_t *es7210_codec = es7210_codec_new(&es7210_cfg);
|
||
if (es7210_codec == NULL) {
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
// 设置麦克风增益为30dB
|
||
es7210_codec->set_mic_gain(es7210_codec, GAIN_30DB);
|
||
|
||
return ESP_OK;
|
||
}
|
||
```
|
||
|
||
2. **在AudioCodec基类中增加ES7210配置支持**:
|
||
```cpp
|
||
// 修改AudioCodec类,增加双麦克风配置接口
|
||
class AudioCodec {
|
||
public:
|
||
// 现有接口...
|
||
|
||
virtual bool configureDualMicrophone() = 0;
|
||
virtual bool setMicrophoneGain(int gain_db) = 0;
|
||
};
|
||
```
|
||
|
||
3. **在具体编解码器实现中添加ES7210配置**:
|
||
```cpp
|
||
bool Es7210AudioCodec::configureDualMicrophone() {
|
||
return (init_es7210_with_dual_mic(ctrl_if_) == ESP_OK);
|
||
}
|
||
```
|
||
|
||
### 2.2 AEC功能集成到AudioProcessor
|
||
|
||
1. **扩展AudioProcessor类**:
|
||
```cpp
|
||
// 修改audio_processor.h
|
||
class AudioProcessor {
|
||
private:
|
||
aec_handle_t *aec_instance_ = nullptr;
|
||
std::vector<int16_t> reference_buffer_;
|
||
|
||
public:
|
||
// 现有方法...
|
||
|
||
// 添加AEC相关方法
|
||
void InitializeAEC(int sample_rate, int channels);
|
||
void ProcessWithAEC(const std::vector<int16_t>& mic_data,
|
||
const std::vector<int16_t>& ref_data,
|
||
std::vector<int16_t>& out_data);
|
||
void SetReferenceAudio(const std::vector<int16_t>& ref_data);
|
||
};
|
||
```
|
||
|
||
2. **实现AEC功能**:
|
||
```cpp
|
||
// 在audio_processor.cc中实现
|
||
void AudioProcessor::InitializeAEC(int sample_rate, int channels) {
|
||
// 使用ESP-SR的AEC API,不依赖ADF
|
||
aec_instance_ = aec_create(sample_rate, 4, channels, AEC_MODE_SR_LOW_COST);
|
||
if (aec_instance_ == nullptr) {
|
||
ESP_LOGE(TAG, "Failed to initialize AEC");
|
||
} else {
|
||
ESP_LOGI(TAG, "AEC initialized successfully");
|
||
}
|
||
}
|
||
|
||
void AudioProcessor::SetReferenceAudio(const std::vector<int16_t>& ref_data) {
|
||
reference_buffer_ = ref_data;
|
||
}
|
||
|
||
void AudioProcessor::ProcessWithAEC(const std::vector<int16_t>& mic_data,
|
||
const std::vector<int16_t>& ref_data,
|
||
std::vector<int16_t>& out_data) {
|
||
if (aec_instance_ == nullptr) {
|
||
// 如果AEC初始化失败,直接返回麦克风数据
|
||
out_data = mic_data;
|
||
return;
|
||
}
|
||
|
||
// 确保输出缓冲区足够大
|
||
out_data.resize(mic_data.size());
|
||
|
||
// 调用AEC处理函数
|
||
size_t processed_size = aec_process(aec_instance_,
|
||
mic_data.data(),
|
||
ref_data.data(),
|
||
out_data.data(),
|
||
mic_data.size() / sizeof(int16_t));
|
||
|
||
if (processed_size == 0) {
|
||
ESP_LOGW(TAG, "AEC processing failed");
|
||
out_data = mic_data; // 失败时返回原始数据
|
||
}
|
||
}
|
||
```
|
||
|
||
3. **修改音频处理流程**:
|
||
```cpp
|
||
void AudioProcessor::AudioProcessorTask() {
|
||
// 现有代码...
|
||
|
||
while (true) {
|
||
// 等待运行标志
|
||
xEventGroupWaitBits(event_group_, PROCESSOR_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY);
|
||
|
||
// 获取麦克风数据
|
||
auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
|
||
if (res == nullptr || res->ret_value == ESP_FAIL) {
|
||
continue;
|
||
}
|
||
|
||
// 准备数据
|
||
std::vector<int16_t> mic_data((int16_t*)res->data,
|
||
(int16_t*)res->data + res->data_size / sizeof(int16_t));
|
||
std::vector<int16_t> processed_data;
|
||
|
||
// AEC处理
|
||
if (aec_instance_ != nullptr && !reference_buffer_.empty()) {
|
||
ProcessWithAEC(mic_data, reference_buffer_, processed_data);
|
||
} else {
|
||
processed_data = mic_data; // 无AEC时使用原始数据
|
||
}
|
||
|
||
// 后续VAD处理和回调...
|
||
// 智能语音打断逻辑(使用处理后的数据)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.3 语音打断功能实现
|
||
|
||
1. **实现智能语音确认机制**:
|
||
```cpp
|
||
// 在audio_processor.h中添加
|
||
struct VoiceInterruptConfig {
|
||
int min_speech_duration_ms = 200; // 最小语音时长,防止回声误触发
|
||
float volume_threshold = 0.05f; // 音量阈值
|
||
};
|
||
|
||
class AudioProcessor {
|
||
private:
|
||
// 现有成员...
|
||
VoiceInterruptConfig interrupt_config_;
|
||
int speech_duration_count_ = 0;
|
||
bool is_voice_interrupt_active_ = false;
|
||
|
||
public:
|
||
// 现有方法...
|
||
|
||
void ConfigureVoiceInterrupt(const VoiceInterruptConfig& config);
|
||
bool DetectVoiceInterrupt(const std::vector<int16_t>& audio_data);
|
||
};
|
||
```
|
||
|
||
2. **实现语音检测逻辑**:
|
||
```cpp
|
||
// 在audio_processor.cc中实现
|
||
void AudioProcessor::ConfigureVoiceInterrupt(const VoiceInterruptConfig& config) {
|
||
interrupt_config_ = config;
|
||
}
|
||
|
||
bool AudioProcessor::DetectVoiceInterrupt(const std::vector<int16_t>& audio_data) {
|
||
// 计算音频能量
|
||
float energy = 0.0f;
|
||
for (int16_t sample : audio_data) {
|
||
float normalized = (float)sample / 32768.0f;
|
||
energy += normalized * normalized;
|
||
}
|
||
energy /= audio_data.size();
|
||
|
||
// 音量检测
|
||
bool volume_detected = (energy > interrupt_config_.volume_threshold * interrupt_config_.volume_threshold);
|
||
|
||
if (volume_detected) {
|
||
speech_duration_count_ += 16; // 假设每帧16ms
|
||
} else {
|
||
speech_duration_count_ = 0;
|
||
}
|
||
|
||
// 语音确认:持续时间超过阈值
|
||
bool voice_confirmed = (speech_duration_count_ >= interrupt_config_.min_speech_duration_ms);
|
||
|
||
if (voice_confirmed && !is_voice_interrupt_active_) {
|
||
is_voice_interrupt_active_ = true;
|
||
return true; // 触发语音打断
|
||
} else if (!volume_detected) {
|
||
is_voice_interrupt_active_ = false;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
```
|
||
|
||
3. **集成到处理流程**:
|
||
```cpp
|
||
void AudioProcessor::AudioProcessorTask() {
|
||
// 现有代码...
|
||
|
||
while (true) {
|
||
// 获取并处理音频数据...
|
||
|
||
// 调用语音打断检测
|
||
if (DetectVoiceInterrupt(processed_data)) {
|
||
ESP_LOGI(TAG, "Voice interrupt detected!");
|
||
// 触发打断回调
|
||
if (voice_interrupt_callback_) {
|
||
voice_interrupt_callback_();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.4 内存优化策略
|
||
|
||
1. **SPIRAM使用优化**:
|
||
```cpp
|
||
// 修改缓冲区分配方式
|
||
void AudioProcessor::Initialize(int sample_rate, int channels) {
|
||
// 现有初始化代码...
|
||
|
||
// 使用SPIRAM分配大缓冲区
|
||
size_t buffer_size = sample_rate * channels * 2; // 2秒的缓冲区
|
||
reference_buffer_.resize(buffer_size);
|
||
|
||
// 确保AEC使用正确的内存
|
||
// AEC内部会使用heap_caps_aligned_alloc,需要确保配置正确
|
||
}
|
||
```
|
||
|
||
2. **配置调整**:
|
||
- 修改`sdkconfig.defaults`以优化SPIRAM使用:
|
||
```
|
||
# 启用SPIRAM
|
||
CONFIG_SPIRAM=y
|
||
|
||
# 优化内存分配策略
|
||
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=8192
|
||
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65536
|
||
|
||
# 增加任务栈大小
|
||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||
```
|
||
|
||
## 3. 注意事项
|
||
|
||
### 3.1 硬件兼容性注意事项
|
||
|
||
- **麦克风选择**:确保正确配置ES7210的MIC1和MIC3(或其他麦克风组合)
|
||
- **增益设置**:根据实际硬件调整麦克风增益,建议从30dB开始测试
|
||
- **参考音频通道**:确保提供正确的扬声器参考音频给AEC算法
|
||
|
||
### 3.2 软件实现注意事项
|
||
|
||
- **采样率一致性**:确保AEC、麦克风输入和参考音频使用相同的采样率
|
||
- **内存对齐**:AEC处理需要16字节对齐的内存,使用`heap_caps_aligned_alloc`
|
||
- **实时性能**:AEC处理较为耗时,确保设置适当的任务优先级
|
||
- **错误处理**:添加完善的错误处理和回退机制
|
||
|
||
### 3.3 调优建议
|
||
|
||
1. **AEC参数调优**:
|
||
- 调整`filter_length`参数(推荐值:4)
|
||
- 根据实际使用场景选择合适的AEC模式
|
||
|
||
2. **VAD参数调优**:
|
||
- 调整语音打断的最小持续时间(建议200ms以上)
|
||
- 根据环境噪声调整音量阈值
|
||
|
||
3. **性能监控**:
|
||
- 添加CPU使用率监控
|
||
- 监控内存使用情况,避免内存泄漏
|
||
|
||
## 4. 测试方案
|
||
|
||
1. **基础功能测试**:
|
||
- 验证ES7210双麦克风配置是否正确
|
||
- 确认AEC初始化和处理无错误
|
||
|
||
2. **AEC性能测试**:
|
||
- 在不同距离测试回声消除效果
|
||
- 在不同音量下测试性能
|
||
|
||
3. **语音打断测试**:
|
||
- 测试在设备播放时的语音打断功能
|
||
- 测试不同距离和音量下的打断灵敏度
|
||
|
||
4. **稳定性测试**:
|
||
- 长时间运行测试,监控内存泄漏
|
||
- 测试在各种环境噪声条件下的稳定性
|
||
|
||
## 5. 总结
|
||
|
||
本移植方案通过直接使用ESP-SR组件提供的AEC API,无需启用完整的ADF架构,即可实现从Airhub_Rtc_h到Kapi_Rtc的AEC语音打断功能移植。方案保留了Kapi_Rtc现有的音频处理架构,通过扩展AudioProcessor类和相关接口,实现了双麦克风配置、AEC处理和智能语音打断功能。
|
||
|
||
通过合理的内存优化和参数调优,可以在保证系统稳定性的同时,实现良好的回声消除和语音打断效果。 |