目标:把传统 DSP 链路里的 NS 这格替换为神经网络(RNNoise),AEC/AGC 仍用 WebRTC,各取所长。 现状:三段已分别跑通并验证;合成场景下 AEC3 双讲过度抑制近端(见末尾"已知限制")。
mic ──► [WebRTC APM: AEC3 → NS → AGC2] ──► 上行
(一个库,内部串联,NS 是传统 DSP)
问题:NS 这格(WebRTC 自带)过度抑制近端语音、去噪一般。
┌─────────────────────┐
mic ──► Stage1 │ WebRTC APM (AEC only)│ ──► aec_out(消回声,保近端)
┌──────┘ echo=on, ns=off, ┘
│
▼ resample 32k→48k
│
Stage2 │ RNNoise NS (神经降噪) ──► ns_out(去噪,少削近端)
│ rnnoise_process_frame, 480样本/帧@48k
▼ resample 48k→32k
│
Stage3 │ WebRTC APM (AGC only) ──► 最终输出(增益归一)
└ echo=off, ns=off, gc2=on
关键改动:NS 从 WebRTC 内部移出,换成 RNNoise 独立一级,AEC 和 AGC 仍由 WebRTC 担任。
文件:webrtc-audio-processing/examples/run-offline.cpp
配置(关键:关 NS、关 AGC,只留 AEC):
webrtc::AudioProcessing::Config config;
config.echo_canceller.enabled = true;
config.echo_canceller.mobile_mode = false; // AEC3(嵌入式可用 true=AECM)
config.gain_controller1.enabled = false; // 模拟AGC,离线关
config.gain_controller2.enabled = false; // Stage1 不做AGC
config.noise_suppression.enabled = false; // ★NS 关掉,交给 RNNoise
config.high_pass_filter.enabled = true;
apm->ApplyConfig(config);帧循环(10ms 一帧,32kHz,int16):
// play = 扬声器参考(far),rec = 麦克风(mic)
while (!play_file.eof() && !rec_file.eof()) {
play_file.read(...); rec_file.read(...); // 各读 320 样本(10ms@32k)
apm->ProcessReverseStream(play_frame, ...); // 喂远端参考(AEC 用)
apm->ProcessStream(rec_frame, ...); // 处理 mic(AEC 消回声)
aec_file.write(...); // 输出 aec_out
}文件:rnnoise/examples/rnnoise_demo.c(核心推理在 rnnoise/src/rnnoise.c 的 rnnoise_process_frame)
API(rnnoise/include/rnnoise.h):
int rnnoise_get_frame_size(void); // = 480
DenoiseState* rnnoise_create(RNNModel *model); // model=NULL 用内置权重
float rnnoise_process_frame(DenoiseState *st, float *out, const float *in); // ★一帧进一帧出
void rnnoise_destroy(DenoiseState *st);帧循环(10ms 一帧,48kHz,int16):
#define FRAME_SIZE 480 // 48k 下 10ms
DenoiseState *st = rnnoise_create(NULL);
while (1) {
short tmp[FRAME_SIZE];
fread(tmp, sizeof(short), FRAME_SIZE, f1); // ★s16le, 不是 float
if (feof(f1)) break;
for (i=0;i<FRAME_SIZE;i++) x[i] = tmp[i]; // int16→float
rnnoise_process_frame(st, x, x); // ★神经降噪
for (i=0;i<FRAME_SIZE;i++) tmp[i] = x[i];
fwrite(tmp, sizeof(short), FRAME_SIZE, fout);
}
rnnoise_destroy(st);数据流:PCM 进 → STFT → GRU 网络 → 增益 mask → iSTFT → PCM 出。权重烤在
src/rnnoise_data.c(78MB,由download_model.sh下)。
Stage1→Stage2 的衔接(因为采样率不同,32k↔48k):
# aec_out(32k wav) → 48k s16 raw → RNNoise → 32k wav
ffmpeg -i aec_out.wav -f s16le -ac 1 -ar 48000 aec_out_48k.raw
rnnoise_demo aec_out_48k.raw ns_out_48k.raw
ffmpeg -f s16le -ar 48000 -ac 1 -i ns_out_48k.raw -ar 32000 ns_out.wav文件:webrtc-audio-processing/examples/run-offline.cpp(同一程序,换配置)
配置:
config.echo_canceller.enabled = false; // AEC 已在 Stage1 做完
config.gain_controller2.enabled = true; // ★只做 AGC2(固定数字增益+limiter)
config.noise_suppression.enabled = false;// NS 已在 Stage2 做完
config.high_pass_filter.enabled = true;循环同 Stage1,只是 AEC 关、AGC2 开;输入是 ns_out,输出是最终信号。
| 头文件 | 提供 | 来自 |
|---|---|---|
webrtc/api/audio/audio_processing.h |
AudioProcessing、AudioProcessingBuilder、Config(AEC/NS/AGC 全在这里开关) |
WebRTC APM |
rnnoise/include/rnnoise.h |
rnnoise_create/process_frame/destroy、DenoiseState、RNNModel |
RNNoise |
WebRTC 的其他头(
modules/audio_processing/...)是私有实现,不直接用;RNNoise 的src/*.h也是私有,只include/rnnoise.h对外。
| 产物 | 路径 | 作用 |
|---|---|---|
libwebrtc-audio-processing-2.1.dylib / .a |
webrtc-audio-processing/builddir/webrtc/modules/audio_processing/ |
AEC3/AECM + AGC2 + NS(传统DSP) |
librnnoise.0.dylib / .a |
rnnoise/.libs/ |
神经 NS(权重内置) |
run-offline |
webrtc-audio-processing/builddir/examples/ |
AEC / AGC 级(改配置切换) |
rnnoise_demo |
rnnoise/examples/ |
NS 级 |
score.py |
versions/ |
客观评测(DNSMOS+AECMOS+ERLE+电平) |
aec_out.wav / ns_out.wav / final.wav |
versions/v3-ns-comparison/ |
三级中间产物 + 最终输出 |
# RNNoise(autotools)
cd rnnoise && ./autogen.sh && ./configure --enable-examples && make -j4
# → librnnoise.0.dylib + examples/rnnoise_demo
# WebRTC APM(meson)
cd webrtc-audio-processing && meson setup builddir && ninja -C builddir
# → libwebrtc-audio-processing-2.1.dylib + examples/run-offline# Stage1: AEC (run-offline 配置=AEC-only)
run-offline play.raw rec.raw aec_out.raw # 32k s16
# Stage2: RNNoise NS (需 48k)
ffmpeg -i aec_out.wav -f s16le -ar 48000 a48.raw
rnnoise_demo a48.raw ns48.raw
ffmpeg -f s16le -ar 48000 -i ns48.raw -ar 32000 ns_out.wav
# Stage3: AGC (run-offline 配置=AGC-only)
ffmpeg -i ns_out.wav -f s16le -ar 32000 ns.raw
run-offline play.raw ns.raw final.raw # play 作 dummy reverse(AEC关)
# 评测
python versions/score.py rec.wav final.wav play.wav- 三段是 shell+ffmpeg 拼接,不是单二进制:Stage1/3 共用
run-offline.cpp(改配置重编切换),Stage2 是rnnoise_demo。中间有 32k↔48k 重采样。 - 合成场景 AEC3 双讲过度抑制近端:冷启动 + 无
render_delay配置,AEC3 延迟估计锁不住,残差抑制器误消近端(近端相关 0.83→0.12)。AECM 则直通不收敛。→ AEC 客观评测必须用真实数据(8295 录音或 AEC Challenge),合成不可信。 - 之前 v3 的 NS 对比 SIG 结论受此污染,BAK(去噪)结论相对可信。
把三段合成一个 C++ 程序,直接链两个库,在内存里按帧流转(不再走 ffmpeg):
// 伪代码:单程序集成
auto apm_aec = AudioProcessingBuilder().Create(); // AEC-only 配置
auto apm_agc = AudioProcessingBuilder().Create(); // AGC-only 配置
DenoiseState *ns = rnnoise_create(NULL);
while (有 10ms 帧) {
apm_aec->ProcessReverseStream(far_frame, ...); // AEC: 喂参考
apm_aec->ProcessStream(mic_frame, ...); // AEC: 消回声
// 32k→48k 重采样(SRC,48k 的 480 样本 = 32k 的 320 样本时长)
rnnoise_process_frame(ns, ns_out, aec_out_48k); // NS: 神经降噪
// 48k→32k 重采样
apm_agc->ProcessStream(ns_out_32k, ...); // AGC: 增益归一
// 输出
}要点:
- 帧对齐:AEC/AGC 是 10ms@32k=320 样本;RNNoise 是 10ms@48k=480 样本;两者时长相同,中间用 SRC 转采样率。
- AEC 的
render_delay要按真实通路配(8295 上扬声器→mic 的真实延时),否则 AEC3 锁不住。 - AEC 在 8295 上持续运行(不冷启动),双讲抑制会轻很多。
- RNNoise 权重
rnnoise_data.c编进固件(78MB,可考虑量化压缩)。
~/Documents/ai-audio/
├── rnnoise/ # 神经 NS
│ ├── include/rnnoise.h # ★唯一对外头
│ ├── src/rnnoise.c # rnnoise_process_frame 实现
│ ├── .libs/librnnoise.0.dylib # ★产物
│ └── examples/rnnoise_demo.c # NS 级 demo
├── webrtc-audio-processing/ # AEC + AGC
│ ├── webrtc/api/audio/audio_processing.h # ★APM 对外头
│ ├── webrtc/modules/audio_processing/{aec3,aecm,agc2,ns}/
│ ├── builddir/.../libwebrtc-audio-processing-2.1.dylib # ★产物
│ └── examples/run-offline.cpp # AEC/AGC 级(改配置切换)
├── versions/
│ ├── score.py # ★客观评测
│ ├── v1-rnnoise-ns/ # RNNoise 单独 NS
│ ├── v2-webrtc-aec-12s/ # WebRTC 全链路
│ ├── v3-ns-comparison/ # NS 对比 + 最终优化链路
│ └── archive/ # 早期实验
└── SOLUTION.md # 本文档