Skip to content

Commit 91c0b5a

Browse files
committed
audio: webrtc_aec: add AECm echo cancellation module
Add the webrtc_aec SOF module wrapping WebRTC AECm (Acoustic Echo Canceller Mobile) — the fixed-point Q15 echo canceller designed for mobile and embedded devices. Module characteristics: - Dual-source (2 input pins): pin 0 = microphone capture, pin 1 = playback echo reference - Single-sink: echo-cancelled microphone output - Fixed-point Q15; no FPU required - Operates at 8 or 16 kHz on 10 ms frames - Supports S16_LE and S32_LE, up to WEBRTC_AEC_CHANNELS_MAX channels (one AecmCore handle per channel) - Pipeline-ID heuristic (matching google_rtc_audio_processing) used to distinguish mic vs. reference at prepare() time - Two backends: real (AECm) and pass-through stub for CI/LLEXT Pin binding follows the same pattern as google-rtc-aec: both DAI capture copiers are connected as sources; the firmware resolves mic-vs-ref from pipeline membership. Reference topology: tools/topology/topology2/development/ cavs-nocodec-webrtc-aec.conf (dual SSP: SSP0=mic, SSP2=ref) UUID: e5da7b5b-133a-ba46-b517651d6300bb83 Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
1 parent ac03eec commit 91c0b5a

10 files changed

Lines changed: 948 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
3+
if(CONFIG_COMP_WEBRTC_AEC STREQUAL "m" AND DEFINED CONFIG_LLEXT)
4+
add_subdirectory(llext ${PROJECT_BINARY_DIR}/webrtc_aec_llext)
5+
add_dependencies(app webrtc_aec)
6+
else()
7+
add_local_sources(sof webrtc_aec.c)
8+
if(CONFIG_COMP_WEBRTC_AEC_STUB)
9+
add_local_sources(sof webrtc_aec-stub.c)
10+
else()
11+
add_local_sources(sof webrtc_aec-aecm.c)
12+
endif()
13+
endif()

src/audio/webrtc_aec/Kconfig

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
3+
config COMP_WEBRTC_AEC
4+
tristate "WebRTC AECm — fixed-point Acoustic Echo Canceller"
5+
help
6+
Select to include the WebRTC AECm (Mobile) acoustic echo canceller.
7+
AECm is a fixed-point Q15 implementation derived from the WebRTC
8+
audio processing library. It requires no floating-point unit and is
9+
well suited to Xtensa HiFi3/HiFi4 class DSPs.
10+
11+
The module accepts two input streams (2 input pins):
12+
Pin 0 — microphone capture (same pipeline as the output)
13+
Pin 1 — playback echo reference (from the render pipeline)
14+
and produces one denoised microphone output stream.
15+
16+
This module is intended as an open-source drop-in replacement for
17+
the google_rtc_audio_processing module that requires a proprietary
18+
library. Both implement the same two-input / one-output topology so
19+
topologies are interchangeable.
20+
21+
The real backend requires webrtc-audio-processing 0.3.x source
22+
(west module modules/audio/webrtc-apm, same as COMP_WEBRTC_NS).
23+
For CI/testing without that source, select COMP_WEBRTC_AEC_STUB.
24+
25+
if COMP_WEBRTC_AEC
26+
27+
config COMP_WEBRTC_AEC_STUB
28+
bool "WebRTC AECm stub backend (no WebRTC source dependency)"
29+
default y if COMP_STUBS
30+
help
31+
Build the webrtc_aec module against a dependency-free stub backend
32+
that passes microphone audio through without modification. Useful
33+
for CI validation, topology testing, and LLEXT packaging without
34+
the cross-built WebRTC AECm archive.
35+
36+
config WEBRTC_AEC_FILTER_LEN_MS
37+
int "AECm adaptive filter length in ms (32, 64 or 128)"
38+
default 64
39+
help
40+
Length of the adaptive echo-cancellation filter. Longer filters
41+
can handle larger acoustic echo delays (e.g. in rooms with long
42+
reverb tails) at the cost of increased memory and CPU.
43+
Valid values: 32, 64, 128.
44+
45+
config WEBRTC_AEC_SUPPRESSION_LEVEL
46+
int "AECm comfort-noise suppression level (0=off, 1=low, 2=medium)"
47+
range 0 2
48+
default 1
49+
help
50+
Controls how aggressively residual echo / background noise is
51+
suppressed in the CNG (comfort noise generator) post-filter:
52+
0 - disabled (only echo suppression, no CNG)
53+
1 - low suppression (recommended)
54+
2 - medium suppression
55+
56+
config WEBRTC_AEC_SAMPLE_RATE_HZ
57+
int "AECm processing sample rate in Hz"
58+
default 16000
59+
help
60+
AECm operates at 8000 or 16000 Hz. When the pipeline runs at a
61+
higher rate (e.g. 48 kHz), the module downsamples before AEC
62+
processing and upsamples afterward. 16 kHz is recommended for
63+
best echo suppression quality.
64+
Valid values: 8000, 16000.
65+
66+
endif # COMP_WEBRTC_AEC
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) 2026 Intel Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
if(CONFIG_COMP_WEBRTC_AEC_STUB)
5+
sof_llext_build("webrtc_aec"
6+
SOURCES ../webrtc_aec.c
7+
../webrtc_aec-stub.c
8+
LIB openmodules
9+
)
10+
else()
11+
include(${CMAKE_CURRENT_LIST_DIR}/../webrtc_aec.cmake)
12+
13+
sof_llext_build("webrtc_aec"
14+
SOURCES ../webrtc_aec.c
15+
../webrtc_aec-aecm.c
16+
INCLUDES "${WEBRTC_AECM_INSTALL_DIR}/include"
17+
LIBS_PATH "${WEBRTC_AECM_INSTALL_DIR}/lib"
18+
LIBS webrtc_aecm
19+
LIB openmodules
20+
)
21+
22+
add_dependencies(webrtc_aec_llext_lib webrtc_aecm_ext)
23+
add_dependencies(webrtc_aec webrtc_aecm_ext)
24+
endif()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include <tools/rimage/config/platform.toml>
2+
#define LOAD_TYPE "2"
3+
#include "../webrtc_aec.toml"
4+
5+
[module]
6+
count = __COUNTER__
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
//
3+
// Copyright(c) 2026 Intel Corporation.
4+
//
5+
// Real AECm backend for the webrtc_aec module.
6+
//
7+
// Wraps WebRtcAecm_Create/Init/BufferFarend/Process from the WebRTC AECm
8+
// (Mobile) fixed-point library extracted from webrtc-audio-processing 0.3.x.
9+
//
10+
// API summary:
11+
// WebRtcAecm_Create() — allocate AecmCore handle
12+
// WebRtcAecm_Init() — configure sample rate (8000 or 16000 Hz)
13+
// WebRtcAecm_BufferFarend() — queue 10 ms of echo reference (far-end)
14+
// WebRtcAecm_Process() — cancel echo from near-end mic frame
15+
// WebRtcAecm_Enable() — configure filter length and CNG suppression
16+
// WebRtcAecm_Free() — release handle
17+
//
18+
// All operations are Q15 fixed-point on int16_t. One handle per channel.
19+
20+
#include <sof/audio/module_adapter/module/generic.h>
21+
#include <errno.h>
22+
#include "webrtc_aec.h"
23+
24+
#include <echo_control_mobile.h> /* from cross-built webrtc-audio-processing 0.3.x */
25+
26+
LOG_MODULE_DECLARE(webrtc_aec, CONFIG_SOF_LOG_LEVEL);
27+
28+
struct webrtc_aec_real_data {
29+
void *aecm[WEBRTC_AEC_CHANNELS_MAX];
30+
int num_channels;
31+
int filter_len_ms;
32+
int suppression;
33+
};
34+
35+
static int webrtc_aec_real_init(struct processing_module *mod)
36+
{
37+
struct webrtc_aec_comp_data *cd = module_get_private_data(mod);
38+
struct webrtc_aec_real_data *rd;
39+
40+
rd = mod_zalloc(mod, sizeof(*rd));
41+
if (!rd)
42+
return -ENOMEM;
43+
44+
cd->backend_data = rd;
45+
comp_info(mod->dev, "webrtc_aec: AECm real backend initialised");
46+
return 0;
47+
}
48+
49+
static int webrtc_aec_real_configure(struct processing_module *mod, int sample_rate_hz,
50+
int filter_len_ms, int suppression, int num_channels)
51+
{
52+
struct webrtc_aec_comp_data *cd = module_get_private_data(mod);
53+
struct webrtc_aec_real_data *rd = cd->backend_data;
54+
AecmConfig config;
55+
int c, ret;
56+
57+
/* Free previously allocated handles. */
58+
for (c = 0; c < rd->num_channels; c++) {
59+
if (rd->aecm[c]) {
60+
WebRtcAecm_Free(rd->aecm[c]);
61+
rd->aecm[c] = NULL;
62+
}
63+
}
64+
rd->num_channels = 0;
65+
rd->filter_len_ms = filter_len_ms;
66+
rd->suppression = suppression;
67+
68+
for (c = 0; c < num_channels; c++) {
69+
rd->aecm[c] = WebRtcAecm_Create();
70+
if (!rd->aecm[c]) {
71+
comp_err(mod->dev, "webrtc_aec: WebRtcAecm_Create() failed ch%d", c);
72+
goto err;
73+
}
74+
75+
ret = WebRtcAecm_Init(rd->aecm[c], sample_rate_hz);
76+
if (ret) {
77+
comp_err(mod->dev,
78+
"webrtc_aec: WebRtcAecm_Init(ch%d, %d) failed %d",
79+
c, sample_rate_hz, ret);
80+
goto err;
81+
}
82+
83+
config.cngMode = suppression;
84+
config.echoMode = (filter_len_ms == 128) ? 4 :
85+
(filter_len_ms == 64) ? 3 :
86+
(filter_len_ms == 32) ? 2 : 3;
87+
ret = WebRtcAecm_set_config(rd->aecm[c], config);
88+
if (ret) {
89+
comp_err(mod->dev, "webrtc_aec: set_config ch%d failed %d", c, ret);
90+
goto err;
91+
}
92+
}
93+
94+
rd->num_channels = num_channels;
95+
comp_info(mod->dev, "webrtc_aec: AECm rate=%d filter=%dms cng=%d ch=%d",
96+
sample_rate_hz, filter_len_ms, suppression, num_channels);
97+
return 0;
98+
99+
err:
100+
for (c = 0; c < num_channels; c++) {
101+
if (rd->aecm[c]) {
102+
WebRtcAecm_Free(rd->aecm[c]);
103+
rd->aecm[c] = NULL;
104+
}
105+
}
106+
return -ENOMEM;
107+
}
108+
109+
static int webrtc_aec_real_process_ch(struct processing_module *mod,
110+
const int16_t *mic, const int16_t *ref, int16_t *out,
111+
int frame_samples, int ch)
112+
{
113+
struct webrtc_aec_comp_data *cd = module_get_private_data(mod);
114+
struct webrtc_aec_real_data *rd = cd->backend_data;
115+
int ret;
116+
117+
/* Queue the far-end (reference/playback) frame first. */
118+
ret = WebRtcAecm_BufferFarend(rd->aecm[ch], ref, frame_samples);
119+
if (ret) {
120+
comp_err(mod->dev, "webrtc_aec: BufferFarend ch%d failed %d", ch, ret);
121+
return ret;
122+
}
123+
124+
/* Process near-end (mic) and produce echo-cancelled output.
125+
* The third parameter (near_end_noiseless) can be NULL. */
126+
ret = WebRtcAecm_Process(rd->aecm[ch], mic, NULL, out, frame_samples, 0);
127+
if (ret) {
128+
comp_err(mod->dev, "webrtc_aec: Process ch%d failed %d", ch, ret);
129+
return ret;
130+
}
131+
132+
return 0;
133+
}
134+
135+
static int webrtc_aec_real_reset(struct processing_module *mod)
136+
{
137+
struct webrtc_aec_comp_data *cd = module_get_private_data(mod);
138+
struct webrtc_aec_real_data *rd = cd->backend_data;
139+
int c, ret;
140+
141+
for (c = 0; c < rd->num_channels; c++) {
142+
if (!rd->aecm[c])
143+
continue;
144+
ret = WebRtcAecm_Init(rd->aecm[c], cd->proc_rate);
145+
if (ret)
146+
comp_warn(mod->dev, "webrtc_aec: reset ch%d failed %d", c, ret);
147+
}
148+
return 0;
149+
}
150+
151+
static int webrtc_aec_real_free(struct processing_module *mod)
152+
{
153+
struct webrtc_aec_comp_data *cd = module_get_private_data(mod);
154+
struct webrtc_aec_real_data *rd = cd->backend_data;
155+
int c;
156+
157+
if (!rd)
158+
return 0;
159+
160+
for (c = 0; c < rd->num_channels; c++) {
161+
if (rd->aecm[c]) {
162+
WebRtcAecm_Free(rd->aecm[c]);
163+
rd->aecm[c] = NULL;
164+
}
165+
}
166+
mod_free(mod, rd);
167+
cd->backend_data = NULL;
168+
return 0;
169+
}
170+
171+
const struct webrtc_aec_backend webrtc_aec_backend = {
172+
.name = "aecm",
173+
.init = webrtc_aec_real_init,
174+
.configure = webrtc_aec_real_configure,
175+
.process_ch = webrtc_aec_real_process_ch,
176+
.reset = webrtc_aec_real_reset,
177+
.free = webrtc_aec_real_free,
178+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
//
3+
// Copyright(c) 2026 Intel Corporation.
4+
//
5+
// Stub backend for webrtc_aec: passes mic audio through unchanged.
6+
// Reference audio is consumed and discarded.
7+
8+
#include <sof/audio/module_adapter/module/generic.h>
9+
#include <rtos/string.h>
10+
#include <errno.h>
11+
#include "webrtc_aec.h"
12+
13+
LOG_MODULE_DECLARE(webrtc_aec, CONFIG_SOF_LOG_LEVEL);
14+
15+
static int webrtc_aec_stub_init(struct processing_module *mod)
16+
{
17+
comp_info(mod->dev, "webrtc_aec stub: no AECm library linked");
18+
return 0;
19+
}
20+
21+
static int webrtc_aec_stub_configure(struct processing_module *mod, int sample_rate_hz,
22+
int filter_len_ms, int suppression, int num_channels)
23+
{
24+
comp_info(mod->dev, "webrtc_aec stub: rate=%d filter=%dms sup=%d ch=%d (ignored)",
25+
sample_rate_hz, filter_len_ms, suppression, num_channels);
26+
return 0;
27+
}
28+
29+
static int webrtc_aec_stub_process_ch(struct processing_module *mod,
30+
const int16_t *mic, const int16_t *ref, int16_t *out,
31+
int frame_samples, int ch)
32+
{
33+
/* Pass mic straight to output; ref is silently discarded. */
34+
memcpy(out, mic, (size_t)frame_samples * sizeof(int16_t));
35+
(void)ref;
36+
(void)ch;
37+
return 0;
38+
}
39+
40+
static int webrtc_aec_stub_reset(struct processing_module *mod)
41+
{
42+
(void)mod;
43+
return 0;
44+
}
45+
46+
static int webrtc_aec_stub_free(struct processing_module *mod)
47+
{
48+
(void)mod;
49+
return 0;
50+
}
51+
52+
const struct webrtc_aec_backend webrtc_aec_backend = {
53+
.name = "stub",
54+
.init = webrtc_aec_stub_init,
55+
.configure = webrtc_aec_stub_configure,
56+
.process_ch = webrtc_aec_stub_process_ch,
57+
.reset = webrtc_aec_stub_reset,
58+
.free = webrtc_aec_stub_free,
59+
};

0 commit comments

Comments
 (0)