Skip to content

Commit b4cfc8b

Browse files
committed
audio: webrtc_ns2: add RNNoise deep-learning noise suppressor module
Add the webrtc_ns2 SOF module wrapping xiph/rnnoise — a recurrent neural network (GRU-based) noise suppressor originally developed by Jean-Marc Valin at Mozilla. Module characteristics: - Single-source, single-sink PCM effect - Floating-point; internal float math, hardware FPU beneficial - Hard requirement: pipeline sample rate MUST be 48000 Hz - Processes 480-sample (10 ms) frames; partial periods accumulated internally - Supports S16_LE and S32_LE, mono or stereo (one DenoiseState per channel; up to WEBRTC_NS2_CHANNELS_MAX channels) - Float scale bridging: pipeline samples normalised to ±1.0 are scaled to RNNoise's native ±32768 range and back - VAD dual-use: rnnoise_process_frame() returns per-frame speech probability [0.0, 1.0]; this fires NOTIFIER_ID_VAD events at a configurable threshold (WEBRTC_NS2_VAD_THRESHOLD_PCT) - Two backends: real (RNNoise) and pass-through stub for CI/LLEXT Design notes: - rnnoise_init() used at prepare/reset instead of rnnoise_create() to avoid per-init heap allocation (embedded-friendly path) - Model weights (rnn_data.c) are const float[] placed in .rodata (Flash/ROM); ~85-340 KB depending on model variant - Peak stack ~12-16 KB per rnnoise_process_frame() call - No expf/tanhf in hot path: replaced by 201-entry tansig LUT Build: - RNNoise cross-compiled by src/audio/webrtc_ns2/webrtc_ns2.cmake (6 C files: denoise.c rnn.c rnn_data.c pitch.c celt_lpc.c kiss_fft.c) - Source fetched via west (modules/audio/rnnoise, SHA 70f1d256) - LLEXT packaging supported UUID: eacfacdc-2a87-c942-97e894c917a740db Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
1 parent 91c0b5a commit b4cfc8b

10 files changed

Lines changed: 857 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_NS2 STREQUAL "m" AND DEFINED CONFIG_LLEXT)
4+
add_subdirectory(llext ${PROJECT_BINARY_DIR}/webrtc_ns2_llext)
5+
add_dependencies(app webrtc_ns2)
6+
else()
7+
add_local_sources(sof webrtc_ns2.c)
8+
if(CONFIG_COMP_WEBRTC_NS2_STUB)
9+
add_local_sources(sof webrtc_ns2-stub.c)
10+
else()
11+
add_local_sources(sof webrtc_ns2-rnn.c)
12+
endif()
13+
endif()

src/audio/webrtc_ns2/Kconfig

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
3+
config COMP_WEBRTC_NS2
4+
tristate "RNNoise deep-learning noise suppressor (floating-point)"
5+
help
6+
Select to include the RNNoise module — a recurrent neural network
7+
noise suppressor originally developed by Jean-Marc Valin (Mozilla /
8+
Opus). RNNoise operates at 48 kHz on 10 ms (480-sample) mono frames
9+
and achieves significantly better suppression of non-stationary noise
10+
(music, babble, keyboard) than the classic spectral Wiener filter
11+
(COMP_WEBRTC_NS).
12+
13+
The module is single-input, single-output and internally emits a
14+
per-frame VAD probability (0.0–1.0) via NOTIFIER_ID_VAD, making it
15+
a combined denoiser and activity detector.
16+
17+
Requirements:
18+
- Floating-point unit (FPU=y on ACE30/PTL; soft-float also works
19+
but at significant CPU cost)
20+
- ~30 KB of state per channel (DenoiseState + RNN weights)
21+
- Pipeline sample rate exactly 48000 Hz
22+
- Pipeline period must be a multiple of 480 samples (10 ms)
23+
24+
The real backend builds RNNoise from source (west module rnnoise,
25+
pinned at commit 70f1d256). For CI/stub builds select
26+
COMP_WEBRTC_NS2_STUB.
27+
28+
License: BSD-3-Clause (xiph/rnnoise).
29+
30+
if COMP_WEBRTC_NS2
31+
32+
config COMP_WEBRTC_NS2_STUB
33+
bool "RNNoise stub backend (no RNNoise source dependency)"
34+
default y if COMP_STUBS
35+
help
36+
Build webrtc_ns2 against a dependency-free pass-through stub instead
37+
of the real RNNoise library. The stub passes audio through unmodified
38+
and always reports VAD probability = 1.0. Used for CI and LLEXT
39+
packaging validation without the RNNoise source.
40+
41+
config WEBRTC_NS2_CHANNELS_MAX
42+
int "Maximum channel count (each channel gets its own RNNoise instance)"
43+
range 1 8
44+
default 2
45+
help
46+
RNNoise is strictly mono; the module runs one DenoiseState per
47+
channel. Increasing this raises SRAM usage by ~30 KB per extra
48+
channel. Set to 1 for pure mono capture pipelines.
49+
50+
config WEBRTC_NS2_VAD_NOTIFY
51+
bool "Emit NOTIFIER_ID_VAD event from RNNoise VAD probability"
52+
default y
53+
help
54+
RNNoise returns a per-frame speech probability in [0.0, 1.0]. When
55+
this option is enabled the module broadcasts a NOTIFIER_ID_VAD event
56+
(speech=1 / silence=0) using a configurable threshold, providing a
57+
combined denoiser + VAD without running a separate webrtc_vad module.
58+
59+
config WEBRTC_NS2_VAD_THRESHOLD_PCT
60+
int "VAD threshold as percentage of max probability (0-100)"
61+
range 0 100
62+
default 50
63+
depends on WEBRTC_NS2_VAD_NOTIFY
64+
help
65+
Speech is declared when rnnoise_process_frame() returns a probability
66+
>= this value / 100.0. 50 is a good default; lower values increase
67+
sensitivity (more false positives), higher values reduce it.
68+
69+
endif # COMP_WEBRTC_NS2
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_NS2_STUB)
5+
sof_llext_build("webrtc_ns2"
6+
SOURCES ../webrtc_ns2.c
7+
../webrtc_ns2-stub.c
8+
LIB openmodules
9+
)
10+
else()
11+
include(${CMAKE_CURRENT_LIST_DIR}/../webrtc_ns2.cmake)
12+
13+
sof_llext_build("webrtc_ns2"
14+
SOURCES ../webrtc_ns2.c
15+
../webrtc_ns2-rnn.c
16+
INCLUDES "${WEBRTC_NS2_INSTALL_DIR}/include"
17+
LIBS_PATH "${WEBRTC_NS2_INSTALL_DIR}/lib"
18+
LIBS rnnoise
19+
LIB openmodules
20+
)
21+
22+
add_dependencies(webrtc_ns2_llext_lib rnnoise_ext)
23+
add_dependencies(webrtc_ns2 rnnoise_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_ns2.toml"
4+
5+
[module]
6+
count = __COUNTER__
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
//
3+
// Copyright(c) 2026 Intel Corporation.
4+
//
5+
// Real RNNoise backend for webrtc_ns2.
6+
//
7+
// RNNoise public API (rnnoise.h / denoise.c):
8+
//
9+
// int rnnoise_get_size(void);
10+
// Returns sizeof(DenoiseState). Use this instead of sizeof() so the
11+
// wrapper doesn't need to know the internal layout.
12+
//
13+
// int rnnoise_init(DenoiseState *st, const RNNModel *model);
14+
// Initialise state in-place. model=NULL uses the built-in weights
15+
// (rnnoise_model_orig compiled into rnnoise_tables.c).
16+
//
17+
// DenoiseState *rnnoise_create(const RNNModel *model);
18+
// Allocate + init. Calls malloc() internally.
19+
//
20+
// void rnnoise_destroy(DenoiseState *st);
21+
// Free. Calls free() internally.
22+
//
23+
// float rnnoise_process_frame(DenoiseState *st, float *out, const float *in);
24+
// Process one 480-sample (10 ms at 48 kHz) mono frame.
25+
// Samples are in full-scale float (not normalised to [-1,1]).
26+
// Returns speech probability in [0, 1].
27+
//
28+
// Memory: ~30 KB per DenoiseState (including RNN state).
29+
// Dependencies: libm (expf, tanhf, sinf, cosf, sqrtf).
30+
31+
#include <sof/audio/module_adapter/module/generic.h>
32+
#include <errno.h>
33+
#include "webrtc_ns2.h"
34+
35+
#include <rnnoise.h> /* from cross-built rnnoise library */
36+
37+
LOG_MODULE_DECLARE(webrtc_ns2, CONFIG_SOF_LOG_LEVEL);
38+
39+
/* RNNoise uses full-scale float (raw amplitude, not normalised). */
40+
#define RNN_FULL_SCALE 32768.0f
41+
42+
struct webrtc_ns2_rnn_data {
43+
DenoiseState *st[WEBRTC_NS2_CHANNELS_MAX];
44+
int num_channels;
45+
};
46+
47+
/* Per-frame scratch buffer at full scale. */
48+
static float rnn_in[WEBRTC_NS2_FRAME_SAMPLES];
49+
static float rnn_out[WEBRTC_NS2_FRAME_SAMPLES];
50+
51+
static int webrtc_ns2_rnn_init(struct processing_module *mod)
52+
{
53+
struct webrtc_ns2_comp_data *cd = module_get_private_data(mod);
54+
struct webrtc_ns2_rnn_data *rd;
55+
56+
rd = mod_zalloc(mod, sizeof(*rd));
57+
if (!rd)
58+
return -ENOMEM;
59+
60+
cd->backend_data = rd;
61+
comp_info(mod->dev, "webrtc_ns2: RNNoise real backend, frame=%d rate=%d",
62+
WEBRTC_NS2_FRAME_SAMPLES, WEBRTC_NS2_SAMPLE_RATE);
63+
return 0;
64+
}
65+
66+
static int webrtc_ns2_rnn_configure(struct processing_module *mod, int num_channels)
67+
{
68+
struct webrtc_ns2_comp_data *cd = module_get_private_data(mod);
69+
struct webrtc_ns2_rnn_data *rd = cd->backend_data;
70+
int c;
71+
72+
/* Free any previous instances from a re-prepare. */
73+
for (c = 0; c < rd->num_channels; c++) {
74+
if (rd->st[c]) {
75+
rnnoise_destroy(rd->st[c]);
76+
rd->st[c] = NULL;
77+
}
78+
}
79+
rd->num_channels = 0;
80+
81+
for (c = 0; c < num_channels; c++) {
82+
/* Pass NULL to use the built-in model weights. */
83+
rd->st[c] = rnnoise_create(NULL);
84+
if (!rd->st[c]) {
85+
comp_err(mod->dev,
86+
"webrtc_ns2: rnnoise_create() failed ch%d", c);
87+
goto err;
88+
}
89+
}
90+
91+
rd->num_channels = num_channels;
92+
comp_info(mod->dev, "webrtc_ns2: %d RNNoise instance(s) created", num_channels);
93+
return 0;
94+
95+
err:
96+
for (c = 0; c < num_channels; c++) {
97+
if (rd->st[c]) {
98+
rnnoise_destroy(rd->st[c]);
99+
rd->st[c] = NULL;
100+
}
101+
}
102+
return -ENOMEM;
103+
}
104+
105+
static float webrtc_ns2_rnn_process_ch(struct processing_module *mod,
106+
const float *in, float *out, int ch)
107+
{
108+
struct webrtc_ns2_comp_data *cd = module_get_private_data(mod);
109+
struct webrtc_ns2_rnn_data *rd = cd->backend_data;
110+
float vad_prob;
111+
int i;
112+
113+
/*
114+
* RNNoise expects full-scale float (raw PCM amplitude), but the SOF
115+
* glue normalises to [-1, +1]. Scale up before processing and back
116+
* down afterward.
117+
*/
118+
for (i = 0; i < WEBRTC_NS2_FRAME_SAMPLES; i++)
119+
rnn_in[i] = in[i] * RNN_FULL_SCALE;
120+
121+
vad_prob = rnnoise_process_frame(rd->st[ch], rnn_out, rnn_in);
122+
123+
for (i = 0; i < WEBRTC_NS2_FRAME_SAMPLES; i++)
124+
out[i] = rnn_out[i] / RNN_FULL_SCALE;
125+
126+
return vad_prob;
127+
}
128+
129+
static int webrtc_ns2_rnn_reset(struct processing_module *mod)
130+
{
131+
struct webrtc_ns2_comp_data *cd = module_get_private_data(mod);
132+
struct webrtc_ns2_rnn_data *rd = cd->backend_data;
133+
int c;
134+
135+
/*
136+
* RNNoise has no reset API; re-initialise each instance in-place
137+
* using rnnoise_init() to clear the GRU hidden state.
138+
*/
139+
for (c = 0; c < rd->num_channels; c++) {
140+
if (rd->st[c])
141+
rnnoise_init(rd->st[c], NULL);
142+
}
143+
return 0;
144+
}
145+
146+
static int webrtc_ns2_rnn_free(struct processing_module *mod)
147+
{
148+
struct webrtc_ns2_comp_data *cd = module_get_private_data(mod);
149+
struct webrtc_ns2_rnn_data *rd = cd->backend_data;
150+
int c;
151+
152+
if (!rd)
153+
return 0;
154+
155+
for (c = 0; c < rd->num_channels; c++) {
156+
if (rd->st[c]) {
157+
rnnoise_destroy(rd->st[c]);
158+
rd->st[c] = NULL;
159+
}
160+
}
161+
mod_free(mod, rd);
162+
cd->backend_data = NULL;
163+
return 0;
164+
}
165+
166+
const struct webrtc_ns2_backend webrtc_ns2_backend = {
167+
.name = "rnnoise",
168+
.init = webrtc_ns2_rnn_init,
169+
.configure = webrtc_ns2_rnn_configure,
170+
.process_ch = webrtc_ns2_rnn_process_ch,
171+
.reset = webrtc_ns2_rnn_reset,
172+
.free = webrtc_ns2_rnn_free,
173+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
//
3+
// Copyright(c) 2026 Intel Corporation.
4+
//
5+
// Stub backend for webrtc_ns2. Passes audio through unchanged.
6+
// Returns VAD probability = 1.0 (always speech).
7+
8+
#include <sof/audio/module_adapter/module/generic.h>
9+
#include <rtos/string.h>
10+
#include "webrtc_ns2.h"
11+
12+
LOG_MODULE_DECLARE(webrtc_ns2, CONFIG_SOF_LOG_LEVEL);
13+
14+
static int webrtc_ns2_stub_init(struct processing_module *mod)
15+
{
16+
comp_info(mod->dev, "webrtc_ns2 stub: no RNNoise library linked");
17+
return 0;
18+
}
19+
20+
static int webrtc_ns2_stub_configure(struct processing_module *mod, int num_channels)
21+
{
22+
comp_info(mod->dev, "webrtc_ns2 stub: ch=%d (ignored)", num_channels);
23+
return 0;
24+
}
25+
26+
static float webrtc_ns2_stub_process_ch(struct processing_module *mod,
27+
const float *in, float *out, int ch)
28+
{
29+
memcpy(out, in, WEBRTC_NS2_FRAME_SAMPLES * sizeof(float));
30+
(void)ch;
31+
return 1.0f; /* always report speech */
32+
}
33+
34+
static int webrtc_ns2_stub_reset(struct processing_module *mod)
35+
{
36+
(void)mod;
37+
return 0;
38+
}
39+
40+
static int webrtc_ns2_stub_free(struct processing_module *mod)
41+
{
42+
(void)mod;
43+
return 0;
44+
}
45+
46+
const struct webrtc_ns2_backend webrtc_ns2_backend = {
47+
.name = "stub",
48+
.init = webrtc_ns2_stub_init,
49+
.configure = webrtc_ns2_stub_configure,
50+
.process_ch = webrtc_ns2_stub_process_ch,
51+
.reset = webrtc_ns2_stub_reset,
52+
.free = webrtc_ns2_stub_free,
53+
};

0 commit comments

Comments
 (0)