配置 AEC 方案
更新时间: 2025/09/04 11:01:41
本文详细介绍 AI 对话功能中的音频回声消除(AEC)方案,包括技术原理、实现方式以及不同方案的选择指导。
前提条件
根据本文操作前,请确保您已经完成了 基础实现流程。
技术名词
本文涉及的技术名词如下:
-
回声消除(Acoustic Echo Cancellation,AEC):指从麦克风采集的音频流中去除回声信号,保留纯净的用户语音。
在 AI 对话场景中,设备的麦克风(MIC)采集到的声音包含两部分:
- 近端信号:用户的说话声音
- 回声信号:从扬声器播放出来的 AI 语音
-
音频参考帧(Audio Reference Frame):是 AEC 算法的关键输入,它提供了扬声器播放内容的精确副本。
通过比较参考帧和麦克风采集的信号,AEC 算法可以:
- 识别信号中的回声成分
- 自适应调整滤波器参数
- 实现精准的回声消除
-
音频 PCM 流:指经过 PCM(Pulse Code Modulation,脉冲编码调制)格式处理的音频数据流。PCM 是将模拟音频信号转换为数字信号的标准方法。
在 AEC 场景中,PCM 流用作音频参考帧,因为 AEC 算法需要精确的原始音频数据来进行回声消除计算。
-
OPUS 编码:是一种开源的音频压缩编码格式,专为实时音频通信而设计。OPUS 结合了 SILK(语音编码)和 CELT(音频编码)两种编码技术的优势。
在 AI 对话场景中,用于压缩用户语音数据,减少网络传输带宽。云端返回的 AI 语音通常也采用 OPUS 编码。相比 PCM,OPUS 编码可将音频数据压缩至原始大小的 10-20% 左右。
AEC 方案
网易云信嵌入式 NERTC SDK 根据设备性能、隐私性要求、实时性要求,提供三种 AEC 方案,您可以参考下表选择其中一种方案:
| 方案 | 处理位置 | 设备性能要求 | 数据传输 | 回声消除效果 | 自动打断支持 | 手动打断支持 |
|---|---|---|---|---|---|---|
| 云端 AEC | 云端服务器 | 低 | 需传输参考帧 | 优秀 | ✅ | ✅ |
| 本地 AEC | 设备端 | 中等 | 仅传输处理后音频 | 良好 | ✅ | ✅ |
| 无 AEC | - | 最低 | 仅传输原始音频 | 无 | ❌ | ✅ |
云端 AEC 方案
此方案将 AEC 处理放在云端进行,对端侧设备性能要求较低,但需要额外传输一路音频参考帧。
- 发送:
- 调用
nertc_push_audio_frame发送本地麦克风采集的音频 PCM 流。 - 调用
nertc_push_audio_reference_frame发送正在播放的远端音频流作为参考帧。
- 调用
- 接收:通过
on_audio_encoded_data回调接收云端返回的 AI 音频流。 - 打断:支持自动打断(用户说话时,AI 自动停止播放),也可调用
nertc_ai_manual_interrupt手动打断。
sequenceDiagram
participant Device as 设备
participant Cloud as 云端AEC
participant AI as AI服务
Device->>Cloud: 调用 `nertc_push_audio_frame` <br> 发送本地麦克风采集的音频 PCM 流
Device->>Cloud: 调用 `nertc_push_audio_reference_frame` <br> 发送正在播放的远端音频流作为参考帧
Cloud->>Cloud: 执行AEC处理
Cloud->>AI: 发送处理后的音频
AI->>Cloud: 返回AI音频响应
Cloud->>Device: 通过 `on_audio_encoded_data` <br> 回调接收云端返回的 AI 音频流
Device->>Device: 播放 AI 音频
C++// 发送麦克风采集的音频流
void MyAPPClass::SendAudioData(void* data, int data_len) {
nertc_sdk_audio_encoded_frame_t encoded_frame;
encoded_frame.data = const_cast<unsigned char*>(packet.payload.data());
encoded_frame.length = packet.payload.size();
nertc_sdk_audio_config audio_config = {16000, 1, 160};
nertc_push_audio_encoded_frame(engine_, NERTC_SDK_MEDIA_MAIN_AUDIO,
&audio_config, 100, &encoded_frame);
}
// 接收并处理云端AI音频,同时发送参考帧
void MyAPPClass::OnAudioEncodedData(const nertc_sdk_callback_context_t* ctx,
uint64_t uid,
nertc_sdk_media_stream_e stream_type,
nertc_sdk_audio_encoded_frame_t* encoded_frame,
bool is_mute_packet) {
ESP_LOGI(TAG, "NERtcSDK OnAudioEncodedData");
std::vector<uint8_t> payload_vector;
if(encoded_frame->data) {
payload_vector.assign(encoded_frame->data, encoded_frame->data + encoded_frame->length);
// 解码云端返回的音频数据
std::vector<int16_t> pcm_payload;
AudioDecode(payload_vector, pcm_payload);
// 播放解码后的PCM数据
PlayAudio(pcm_payload);
// 关键步骤:将刚播放的PCM数据作为参考帧发送给SDK
nertc_sdk_audio_frame_t audio_frame;
audio_frame.type = NERTC_SDK_AUDIO_PCM_16;
audio_frame.data = pcm_payload.data();
audio_frame.length = pcm_payload.size();
nertc_push_audio_reference_frame(engine_, NERTC_SDK_MEDIA_MAIN_AUDIO,
encoded_frame, &audio_frame);
}
}
// 支持自动打断,同时支持手动打断
void MyAPPClass::ManualInterrupt() {
nertc_ai_manual_interrupt(engine_);
}
本地 AEC 方案
此方案在设备端完成 AEC 处理,需要设备具备足够的 CPU 处理能力。处理流程相对简单,无需发送参考帧。
- 发送:调用
nertc_push_audio_encoded_frame发送经过本地 AEC 处理和 OPUS 编码后的音频流。 - 接收:通过
on_audio_encoded_data回调接收云端返回的 AI 音频流。 - 打断:支持自动打断,也可调用
nertc_ai_manual_interrupt手动打断。
C++// 发送本地AEC处理和编码后的音频
void MyAPPClass::SendAudioData(void* opus_data, int data_len) {
nertc_sdk_audio_encoded_frame_t encoded_frame;
encoded_frame.data = (unsigned char*)opus_data;
encoded_frame.length = data_len;
nertc_sdk_audio_config audio_config = {MY_AUDIO_SAMPLERATE, MY_AUDIO_CHANNELS, MY_AUDIO_SAMPLES_PER_CHANNEL};
nertc_push_audio_encoded_frame(engine_, NERTC_SDK_MEDIA_MAIN_AUDIO,
&audio_config, 100, &encoded_frame);
}
// 接收AI音频并播放
void MyAPPClass::OnAudioEncodedData(const nertc_sdk_callback_context_t* ctx,
uint64_t uid,
nertc_sdk_media_stream_e stream_type,
nertc_sdk_audio_encoded_frame_t* encoded_frame,
bool is_mute_packet) {
RTC_LOGI(TAG, "NERtcSDK OnAudioEncodedData");
std::vector<uint8_t> payload_vector;
if(encoded_frame->data) {
payload_vector.assign(encoded_frame->data, encoded_frame->data + encoded_frame->length);
std::vector<int16_t> pcm_vector;
AudioDecode(payload_vector, pcm_vector);
// 直接播放,无需发送参考帧
PlayAudio(pcm_payload);
}
}
// 支持自动打断,同时支持手动打断
void MyAPPClass::ManualInterrupt() {
nertc_ai_manual_interrupt(engine_);
}
无 AEC 方案
此方案不进行任何 AEC 处理,适用于没有扬声器播放远端声音的场景(如使用耳机),或者对回声不敏感的业务。
- 发送:调用
nertc_push_audio_encoded_frame发送 OPUS 编码后的音频流。 - 接收:通过
on_audio_encoded_data回调接收音频流。 - 打断:只能通过
nertc_ai_manual_interrupt手动打断。不支持 自动打断。
C++// 发送原始编码音频
void MyAPPClass::SendAudioData(void* data, int data_len) {
nertc_sdk_audio_encoded_frame_t encoded_frame;
encoded_frame.data = (unsigned char*)data;
encoded_frame.length = data_len;
nertc_sdk_audio_config audio_config = {MY_AUDIO_SAMPLERATE, MY_AUDIO_CHANNELS, MY_AUDIO_SAMPLES_PER_CHANNEL};
nertc_push_audio_encoded_frame(engine_, NERTC_SDK_MEDIA_MAIN_AUDIO,
&audio_config, 100, &encoded_frame);
}
// 接收
void MyAPPClass::OnAudioEncodedData(const nertc_sdk_callback_context_t* ctx,
uint64_t uid,
nertc_sdk_media_stream_e stream_type,
nertc_sdk_audio_encoded_frame_t* encoded_frame,
bool is_mute_packet) {
RTC_LOGI(TAG, "NERtcSDK OnAudioEncodedData");
std::vector<uint8_t> payload_vector;
if(encoded_frame->data) {
payload_vector.assign(encoded_frame->data, encoded_frame->data + encoded_frame->length);
std::vector<int16_t> pcm_vector;
AudioDecode(payload_vector, pcm_vector);
// 直接播放
PlayAudio(pcm_payload);
}
}
// 手动打断(无 AEC 方案的唯一打断方式)
void MyAPPClass::ManualInterrupt() {
nertc_ai_manual_interrupt(engine_);
}





