Skip to content

Commit f9033d9

Browse files
committed
audio: webrtc_vad: add GMM voice activity detector module
Add the webrtc_vad SOF module wrapping libfvad — a standalone C port of the WebRTC Gaussian mixture model VAD (identical to the algorithm shipping in Chrome/WebRTC since 2011). Module characteristics: - Single-source, single-sink PCM pass-through - Accumulates 10/20/30 ms frames (configurable via Kconfig) - Broadcasts NOTIFIER_ID_VAD event on every frame classification - Supports S16_LE and S32_LE at 8/16/32/48 kHz, mono or stereo - Fixed-point; no FPU required - Two backends: real (libfvad) and pass-through stub for CI/LLEXT Build: - libfvad is cross-compiled by src/audio/webrtc_vad/webrtc_vad.cmake - Source is fetched via west (modules/audio/libfvad, pinned SHA 532ab666c20d) - LLEXT packaging supported via llext/ subdirectory Add NOTIFIER_ID_VAD to the notifier_id enum so downstream components (e.g. webrtc_ns2 / RNNoise) can share the same VAD event channel. UUID: c790b11d-5d14-e54e-be36ba4ad732cc14 Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
1 parent fb9fd55 commit f9033d9

13 files changed

Lines changed: 1025 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
3+
if(CONFIG_COMP_WEBRTC_VAD STREQUAL "m" AND DEFINED CONFIG_LLEXT)
4+
add_subdirectory(llext ${PROJECT_BINARY_DIR}/webrtc_vad_llext)
5+
add_dependencies(app webrtc_vad)
6+
else()
7+
add_local_sources(sof webrtc_vad.c)
8+
9+
if(CONFIG_COMP_WEBRTC_VAD_STUB)
10+
add_local_sources(sof webrtc_vad-stub.c)
11+
else()
12+
add_local_sources(sof webrtc_vad-fvad.c)
13+
# NOTE: the libc symbols needed by libfvad (malloc/free/memset/...) are
14+
# already provided by the SOF core in built-in and testbench builds.
15+
# For LLEXT the shims are in llext/CMakeLists.txt (webrtc_vad-shims.c).
16+
endif()
17+
endif()

src/audio/webrtc_vad/Kconfig

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
3+
config COMP_WEBRTC_VAD
4+
tristate "WebRTC Voice Activity Detection (libfvad)"
5+
help
6+
Select to include the WebRTC Voice Activity Detection module. It
7+
wraps the libfvad library (a standalone pure-C extraction of the
8+
WebRTC GMM VAD) behind the SOF module interface. The module
9+
inspects each 10/20/30 ms PCM frame and annotates it as speech or
10+
non-speech via a NOTIFIER_ID_VAD notifier event, leaving the audio
11+
stream unmodified (pass-through).
12+
13+
libfvad supports 8, 16, 32 and 48 kHz; it resamples to 8 kHz
14+
internally. The GMM classifier operates in Q15 fixed-point and
15+
requires no FPU.
16+
17+
The real backend requires libfvad cross-built for the target
18+
(west module: modules/audio/libfvad). Without the source, or for
19+
CI/stub builds, select COMP_WEBRTC_VAD_STUB.
20+
21+
if COMP_WEBRTC_VAD
22+
23+
config COMP_WEBRTC_VAD_STUB
24+
bool "WebRTC VAD stub backend (no libfvad dependency)"
25+
default y if COMP_STUBS
26+
help
27+
Build the webrtc_vad module against a dependency-free stub backend
28+
instead of libfvad. The stub always reports speech (VAD = 1) so
29+
the audio data flow and LLEXT packaging can be validated without
30+
the libfvad source or a cross-built archive.
31+
32+
config WEBRTC_VAD_MODE
33+
int "VAD aggressiveness mode (0=quality ... 3=aggressive)"
34+
range 0 3
35+
default 2
36+
help
37+
Sets the libfvad operating mode which controls the trade-off
38+
between false-positive rate and missed-speech rate:
39+
0 - highest quality (fewest false alarms, may miss quiet speech)
40+
1 - moderate quality
41+
2 - moderate aggressiveness (default)
42+
3 - most aggressive (detects most speech, more false alarms)
43+
This option is also used by the stub backend (mode value is ignored
44+
by the stub but must be a valid integer for compilation).
45+
46+
config WEBRTC_VAD_FRAME_MS
47+
int "VAD frame length in milliseconds (10, 20 or 30)"
48+
default 10
49+
help
50+
libfvad processes audio in fixed-size frames. Valid values are
51+
10, 20 and 30 ms. Smaller frames give lower latency; 10 ms is
52+
the recommended default and matches the WebRTC pipeline convention.
53+
The SOF pipeline period must be a multiple of this value (the
54+
module accumulates sub-frames internally if needed).
55+
56+
endif # COMP_WEBRTC_VAD

src/audio/webrtc_vad/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# WebRTC Voice Activity Detection module (`webrtc_vad`)
2+
3+
Wraps [libfvad](https://github.com/dpirch/libfvad) — a standalone pure-C
4+
BSD-3 extraction of the WebRTC GMM Voice Activity Detection algorithm — behind
5+
the SOF `module_interface`.
6+
7+
## What it does
8+
9+
The module is a **pass-through PCM effect**: audio is copied from source to
10+
sink without modification. On each complete VAD frame (10, 20 or 30 ms,
11+
Kconfig-selectable), the module classifies channel-0 audio as speech or
12+
non-speech using the libfvad GMM classifier and broadcasts the binary decision
13+
(`WEBRTC_VAD_SPEECH` / `WEBRTC_VAD_SILENCE`) via `NOTIFIER_ID_VAD`.
14+
15+
Downstream consumers (keyword detection gating, host-side VAD forwarding,
16+
noise suppression activation) subscribe to this notifier without needing to
17+
poll the module.
18+
19+
## Algorithm
20+
21+
libfvad implements the WebRTC Gaussian Mixture Model VAD:
22+
- Operates in **Q15 fixed-point** — no FPU required
23+
- Internally resamples any 8/16/32/48 kHz input to **8 kHz** for classification
24+
- 6 sub-band log-energy features feed a per-band GMM; bands are aggregated to
25+
a single speech/non-speech bit
26+
- Four aggressiveness modes (0 = most conservative, 3 = most aggressive)
27+
28+
## Design
29+
30+
```
31+
webrtc_vad.c — SOF module_interface glue (init/prepare/process/reset/free)
32+
webrtc_vad.h — private data (struct webrtc_vad_comp_data) + backend API
33+
webrtc_vad-stub.c — dependency-free passthrough stub; always reports SPEECH
34+
webrtc_vad-fvad.c — real libfvad backend
35+
webrtc_vad-shims.c — malloc/free/mem* shims for LLEXT (backed by rballoc)
36+
webrtc_vad.cmake — ExternalProject cross-build of libfvad static library
37+
```
38+
39+
The `struct webrtc_vad_backend` interface mirrors `struct ffmpeg_dec_backend`:
40+
a named set of function pointers (`init`, `configure`, `classify`, `reset`,
41+
`free`). The SOF glue calls only these and remains independent of libfvad.
42+
43+
## Build
44+
45+
**Stub (CI/testing):**
46+
```
47+
CONFIG_COMP_WEBRTC_VAD=y # or =m for LLEXT
48+
CONFIG_COMP_WEBRTC_VAD_STUB=y
49+
```
50+
No external dependencies. The stub always reports speech.
51+
52+
**Real libfvad backend:**
53+
```
54+
CONFIG_COMP_WEBRTC_VAD=m # LLEXT
55+
CONFIG_COMP_WEBRTC_VAD_STUB=n
56+
```
57+
Requires `west update` to pull `modules/audio/libfvad` (pinned in
58+
`west.yml`). The cross-build is driven by `webrtc_vad.cmake` invoked from
59+
`llext/CMakeLists.txt`.
60+
61+
## Kconfig options
62+
63+
| Option | Default | Description |
64+
|---|---|---|
65+
| `COMP_WEBRTC_VAD` || Enable module (y=built-in, m=LLEXT) |
66+
| `COMP_WEBRTC_VAD_STUB` | y if COMP_STUBS | Use passthrough stub backend |
67+
| `WEBRTC_VAD_MODE` | 2 | Aggressiveness 0–3 |
68+
| `WEBRTC_VAD_FRAME_MS` | 10 | VAD frame size in ms (10/20/30) |
69+
70+
## Frame accumulation
71+
72+
SOF pipeline periods may be shorter than the VAD frame size. The module
73+
accumulates channel-0 S16 samples in a ring buffer (`accumulator[]`) and
74+
calls `classify()` once per full frame, introducing at most `WEBRTC_VAD_FRAME_MS`
75+
ms of additional latency.
76+
77+
## Status / TODO
78+
79+
- Topology wiring for IPC4 (`.toml` mod_cfg tuning for target platform)
80+
- `set_configuration` handler for runtime mode/frame-ms changes via IPC
81+
- Forward VAD decision to host via a SOF IPC notification message
82+
- Unit test with reference speech/noise WAV files in `west twister`
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) 2026 Intel Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
if(CONFIG_COMP_WEBRTC_VAD_STUB)
5+
# Dependency-free build: SOF glue + passthrough stub backend, no libfvad.
6+
sof_llext_build("webrtc_vad"
7+
SOURCES ../webrtc_vad.c
8+
../webrtc_vad-stub.c
9+
LIB openmodules
10+
)
11+
else()
12+
# Real VAD: cross-build libfvad and link it.
13+
include(${CMAKE_CURRENT_LIST_DIR}/../webrtc_vad.cmake)
14+
15+
sof_llext_build("webrtc_vad"
16+
SOURCES ../webrtc_vad.c
17+
../webrtc_vad-fvad.c
18+
../webrtc_vad-shims.c
19+
INCLUDES "${LIBFVAD_INSTALL_DIR}/include"
20+
LIBS_PATH "${LIBFVAD_INSTALL_DIR}/lib"
21+
LIBS fvad
22+
LIB openmodules
23+
)
24+
25+
# The archive must be built before the module compiles/links against it.
26+
add_dependencies(webrtc_vad_llext_lib fvad_ext)
27+
add_dependencies(webrtc_vad fvad_ext)
28+
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_vad.toml"
4+
5+
[module]
6+
count = __COUNTER__
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
//
3+
// Copyright(c) 2026 Intel Corporation.
4+
//
5+
// libfvad backend for the webrtc_vad module.
6+
//
7+
// libfvad is a standalone pure-C BSD-3 extraction of the WebRTC GMM Voice
8+
// Activity Detection algorithm. It operates in Q15 fixed-point and requires
9+
// no FPU, making it well suited to Xtensa DSPs and Cortex-M class targets.
10+
//
11+
// The backend wraps the following minimal libfvad API:
12+
//
13+
// fvad_new() — allocate a FvadState
14+
// fvad_set_mode() — aggressiveness 0..3
15+
// fvad_set_sample_rate() — 8000/16000/32000/48000 Hz
16+
// fvad_process() — classify num_samples of int16 mono audio
17+
// fvad_free() — release FvadState
18+
//
19+
// Requires cross-compiled libfvad static library and headers (west module
20+
// modules/audio/libfvad; see webrtc_vad.cmake for the cross-build recipe).
21+
// Only compiled when CONFIG_COMP_WEBRTC_VAD_STUB is not selected.
22+
23+
#include <sof/audio/module_adapter/module/generic.h>
24+
#include <errno.h>
25+
#include <stdlib.h>
26+
#include "webrtc_vad.h"
27+
28+
#include <fvad.h> /* from cross-built libfvad install */
29+
30+
LOG_MODULE_DECLARE(webrtc_vad, CONFIG_SOF_LOG_LEVEL);
31+
32+
static int webrtc_vad_fvad_init(struct processing_module *mod)
33+
{
34+
struct webrtc_vad_comp_data *cd = module_get_private_data(mod);
35+
Fvad *fvad;
36+
37+
fvad = fvad_new();
38+
if (!fvad) {
39+
comp_err(mod->dev, "webrtc_vad: fvad_new() failed");
40+
return -ENOMEM;
41+
}
42+
43+
cd->backend_data = fvad;
44+
comp_info(mod->dev, "webrtc_vad: libfvad backend initialised");
45+
return 0;
46+
}
47+
48+
static int webrtc_vad_fvad_configure(struct processing_module *mod,
49+
int sample_rate_hz, int mode)
50+
{
51+
struct webrtc_vad_comp_data *cd = module_get_private_data(mod);
52+
Fvad *fvad = cd->backend_data;
53+
int ret;
54+
55+
ret = fvad_set_sample_rate(fvad, sample_rate_hz);
56+
if (ret < 0) {
57+
comp_err(mod->dev, "webrtc_vad: fvad_set_sample_rate(%d) failed %d",
58+
sample_rate_hz, ret);
59+
return -EINVAL;
60+
}
61+
62+
ret = fvad_set_mode(fvad, mode);
63+
if (ret < 0) {
64+
comp_err(mod->dev, "webrtc_vad: fvad_set_mode(%d) failed %d",
65+
mode, ret);
66+
return -EINVAL;
67+
}
68+
69+
comp_info(mod->dev, "webrtc_vad: libfvad rate=%d mode=%d",
70+
sample_rate_hz, mode);
71+
return 0;
72+
}
73+
74+
static int webrtc_vad_fvad_classify(struct processing_module *mod,
75+
const int16_t *samples, int num_samples)
76+
{
77+
struct webrtc_vad_comp_data *cd = module_get_private_data(mod);
78+
Fvad *fvad = cd->backend_data;
79+
int decision;
80+
81+
/* fvad_process returns 1 (speech), 0 (non-speech), or <0 on error. */
82+
decision = fvad_process(fvad, samples, num_samples);
83+
if (decision < 0) {
84+
comp_err(mod->dev, "webrtc_vad: fvad_process error %d", decision);
85+
return -EIO;
86+
}
87+
88+
return decision;
89+
}
90+
91+
static int webrtc_vad_fvad_reset(struct processing_module *mod)
92+
{
93+
struct webrtc_vad_comp_data *cd = module_get_private_data(mod);
94+
Fvad *fvad = cd->backend_data;
95+
96+
/* libfvad has no explicit reset API; re-init the state via free+new. */
97+
int rate = cd->sample_rate;
98+
int mode = CONFIG_WEBRTC_VAD_MODE;
99+
100+
fvad_free(fvad);
101+
fvad = fvad_new();
102+
if (!fvad)
103+
return -ENOMEM;
104+
105+
cd->backend_data = fvad;
106+
return webrtc_vad_fvad_configure(mod, rate, mode);
107+
}
108+
109+
static int webrtc_vad_fvad_free(struct processing_module *mod)
110+
{
111+
struct webrtc_vad_comp_data *cd = module_get_private_data(mod);
112+
113+
if (cd->backend_data) {
114+
fvad_free(cd->backend_data);
115+
cd->backend_data = NULL;
116+
}
117+
return 0;
118+
}
119+
120+
const struct webrtc_vad_backend webrtc_vad_backend = {
121+
.name = "fvad",
122+
.init = webrtc_vad_fvad_init,
123+
.configure = webrtc_vad_fvad_configure,
124+
.classify = webrtc_vad_fvad_classify,
125+
.reset = webrtc_vad_fvad_reset,
126+
.free = webrtc_vad_fvad_free,
127+
};

0 commit comments

Comments
 (0)