How-To Guide: Customizing Multi-Slot WOV & ECNS Pipelines#
Sound Open Firmware (SOF) features an advanced, multi-pipeline Multi-Slot Wake-on-Voice (WOV) and Echo Cancellation & Noise Suppression (ECNS) subsystem developed on the wcl-uaol-wov-002 branch. This architecture enables simultaneous full-duplex communications and multi-keyword spotting from a single Digital Microphone (DMIC) array.
This comprehensive developer guide details how the subsystem is structured, how inter-module communication is coordinated locklessly via the SOF Notifier, and provides a step-by-step walkthrough on how to replace the ECNS and WOV modules with custom or third-party algorithms, modify scheduling periods, adjust buffer constraints (IBS/OBS), and reconfigure ALSA Topology 2.0.
—
Subsystem Architecture & Signal Flow#
The baseline architecture coordinates eight interconnected pipelines spanning two distinct host audio capture streams originating from a single Digital Microphone (DMIC) physical interface:
ECNS Communication Stream (ALSA PCM 10 / ``hw:0,10``): Captures clean, noise-suppressed stereo 16 kHz speech. This stream is routed to host-side teleconferencing applications (e.g. Teams, Zoom, WebRTC) or audio recorders requiring high-fidelity voice transmission free from acoustic feedback and room reverberation.
WOV Keyword & Audio Stream (ALSA PCM 11 / ``hw:0,11``): Captures pre-roll audio (2.0 seconds of cached historical audio from the Key Phrase Buffer) seamlessly concatenated with live speech from whichever keyword spotting slot triggered detection. This stream feeds host voice assistants (e.g. Alexa, Google Assistant, custom on-device wake-word engines).
A physical 4-channel 16 kHz DMIC array supplies the DSP with two primary channels of near-end acoustic speech (Microphones 0 and 1) and two channels of far-end acoustic echo reference (Speaker Channels 2 and 3 looped back from the playback audio subsystem). By co-locating near-end speech and far-end echo in the same synchronous 4-channel DMIC stream, the hardware guarantees zero sample-drift and deterministic phase alignment between speaker playback and microphone capture.
The audio graph processes this stream through three distinct functional phases:
Phase 1: Low-Latency DAI Capture (Pipeline 100): Ingests raw 4-channel 16 kHz audio from the DMIC hardware at 1 ms intervals (16 frames per tick), maintaining minimum input latency before handing off to processing.
Phase 2: Data Processing & Dual-Pin Separation (Pipelines 105 & 106): Batches samples into a 20 ms period (320 frames). The ECNS module cancels speaker echo using the reference channels and removes ambient noise, generating two independent output streams: - Pin 0 (Clean Mono Speech): Fed to Pipeline 106 (Key Phrase Buffer), which continuously maintains a 2.0-second circular ring buffer in DSP memory. - Pin 1 (Clean Stereo Speech): Fed directly to Pipeline 107 (ECNS Host Copier) for teleconferencing on PCM 10.
Phase 3: Multi-Slot Keyword Detection & Arbitration (Pipelines 101–104): The KPB fans out the mono clean speech across three concurrent detector slots running in 10 ms Data Processing periods. When a slot matches its target keyword model, it triggers the KPB to drain its 2.0-second history buffer and commands the WOV Arbiter to multiplex that slot’s audio to PCM 11.
Figure 223 Figure 322: Multi-Slot WOV & ECNS Pipeline Architecture across 4ch DMIC capture, 20ms DP ECNS, 2.0s KPB pre-roll, 3 concurrent detector slots, WOV Arbiter, and the SOF Notifier event bus.#
Pipeline Graph Breakdown#
Pipeline ID |
Name |
Scheduling Domain |
Components & Signal Flow |
|---|---|---|---|
Pipeline 100 |
DAI Capture |
Core 0 • LL 1 ms (16 frames) |
|
Pipeline 105 |
ECNS Processing |
Core 0 • DP 20 ms (320 frames) |
|
Pipeline 106 |
KPB History Buffer |
Core 0 • DP 20 ms (320 frames) |
|
Pipeline 101 |
WOV Slot 0 |
Core 0 • DP 10 ms (160 frames) |
|
Pipeline 102 |
WOV Slot 1 |
Core 0 • DP 10 ms (160 frames) |
|
Pipeline 103 |
WOV Slot 2 |
Core 1 • DP 10 ms (160 frames) |
|
Pipeline 104 |
WOV Host Capture |
Core 0 • LL 1 ms (16 frames) |
|
Pipeline 107 |
ECNS Host Capture |
Core 0 • LL 1 ms (16 frames) |
|
—
SOF Notifier Inter-Module Event Bus#
A central architectural innovation in this subsystem is the lockless, zero-IPC event dispatch provided by the SOF Notifier. Because all modules execute inside the same DSP address space, inter-module events are dispatched synchronously without serialization or kernel intervention:
Notifier Event ID |
Producer |
Consumer |
Action & Payload |
|---|---|---|---|
|
Triggered Detector (e.g. Slot 0) |
|
Passes |
|
Triggered Detector |
|
Commands KPB to transition to drain mode ( |
|
|
Sibling Detectors |
Broadcasts |
Notifier Registration & Dispatch Pattern#
The SOF Notifier provides a lockless, publish-subscribe event mechanism that coordinates DSP processing components executing within the firmware address space without incurring host IPC latency, kernel transitions, or DMA overhead.
Notifier Registration & Lifecycle#
Modules subscribe to events using notifier_register(), typically during the component’s prepare() or init() lifecycle phases:
#include <sof/lib/notifier.h>
#include <sof/audio/wov_arbiter.h>
#include <sof/audio/kpb.h>
/* Prototype:
* void notifier_register(struct comp_dev *dev, void *caller_data,
* enum notify_id type,
* void (*cb)(void *arg, enum notify_id type, void *data),
* uint32_t flags);
*/
notifier_register(dev, cd, NOTIFIER_ID_WOV_CTRL, my_wov_ctrl_callback, 0);
Key parameters for registration:
dev: Pointer to the component device (struct comp_dev *).caller_data: Private context pointer passed back as the first argument (void *arg) to the callback function (e.g.struct my_wov_comp_data *cd).type: The unique enumeration value identifying the event class (e.g.NOTIFIER_ID_WOV_CTRL).cb: Callback function pointer invoked synchronously whenever matching events are dispatched.flags: Operational modifiers (typically0for standard synchronous delivery).
Warning
Mandatory Unregistration:
Any module that registers a notifier callback must explicitly call notifier_unregister(dev, cd, NOTIFIER_ID_WOV_CTRL) during its reset() and free() lifecycle routines. Failing to unregister leaves dangling function pointers in the global notifier dispatch table, causing fatal DSP exception faults (null-pointer or unmapped memory dereferences) when subsequent events are fired after pipeline teardown.
Callback Execution Context & Rules#
Notifier callbacks are executed synchronously within the thread/task context of the component that dispatches the event via notifier_event():
static void my_wov_ctrl_callback(void *arg, enum notify_id type, void *data)
{
struct my_wov_comp_data *cd = (struct my_wov_comp_data *)arg;
struct wov_ctrl_event_data *ctrl = (struct wov_ctrl_event_data *)data;
if (ctrl->cmd == WOV_CMD_PAUSE) {
cd->paused = true;
} else if (ctrl->cmd == WOV_CMD_RESUME) {
cd->paused = false;
cd->detected = false;
my_model_reset(cd->model_context);
}
}
Because the callback runs directly on the caller’s execution thread, strict real-time rules apply:
Non-Blocking Execution: Callbacks must never sleep, yield, or pend on semaphores or mutexes.
Zero Dynamic Allocation: Never allocate memory (
rballoc,malloc) inside a callback.Minimal State Manipulation: Limit logic to setting volatile state flags, resetting pointer offsets, or triggering internal state machines.
Synchronous Event Dispatch & Payloads#
When a keyword detector achieves confidence scoring above its operational threshold, it issues two back-to-back synchronous notifications:
static void my_wov_trigger_detection(struct comp_dev *dev, int slot_id)
{
struct kpb_event_data kpb_evt = {
.event_id = KPB_EVENT_DRAIN,
.client_id = slot_id,
};
struct wov_detect_event_data arb_evt = {
.slot_id = slot_id,
};
/* 1. Command KPB to drain 2.0 seconds of pre-roll history audio */
notifier_event(dev, NOTIFIER_ID_KPB_CLIENT_EVT,
CORE_SPECIFIC_BROADCAST, &kpb_evt, sizeof(kpb_evt));
/* 2. Inform WOV Arbiter of winning slot to route audio to PCM 11 */
notifier_event(dev, NOTIFIER_ID_WOV_DETECT,
CORE_SPECIFIC_BROADCAST, &arb_evt, sizeof(arb_evt));
}
Dispatch Mechanics:
core_mask: SpecifyingCORE_SPECIFIC_BROADCASTinstructs the notifier infrastructure to dispatch the event locally on the current core and cross-dispatch to secondary cores (e.g. Core 1) via DSP Inter-Processor Interrupts (IPI).NOTIFIER_ID_KPB_CLIENT_EVT: Ingested bykpb.106.1. TheKPB_EVENT_DRAINpayload causes the Key Phrase Buffer to cease circular overwriting and rapidly pump its 64 KB history ring buffer downstream into the active slot’s pipeline.NOTIFIER_ID_WOV_DETECT: Ingested bywov_arbiter.104.1. Theslot_idpayload instructs the arbiter to lock its audio multiplexer to that slot’s input pin, unmute the stream tohost-copier.11(ALSA PCM 11), and broadcast aWOV_CMD_PAUSEevent acrossNOTIFIER_ID_WOV_CTRLto suppress sibling slots from firing.
—
Step-by-Step Guide: Replacing the ECNS Module#
The ECNS module performs Acoustic Echo Cancellation (AEC) and Noise Suppression (NS). Developers can replace the reference ECNS module with custom neural noise suppressors (e.g. RNNoise, DeepFilterNet), open-source algorithms (SpeexDSP, WebRTC AEC3), or proprietary vendor DSP libraries.
Step 1: Understand the Multi-Pin Contract#
In full-duplex communication systems, microphones pick up both the near-end user’s voice and the far-end speaker playback echoing off the room walls and device chassis. To eliminate this acoustic coupling without distorting speech, the ECNS component must ingest both the microphone signals and an uncorrupted echo reference, and provide separate outputs tailored for voice trigger and communications.
An ECNS component in this topology must adhere to a strict multi-pin input/output contract:
Input Pin 0 (4-Channel 16 kHz S16_LE Capture): - Channels 0 and 1 (Microphones): Primary physical microphone signals containing near-end speech, ambient noise, and acoustic speaker echo. - Channels 2 and 3 (Echo Reference): Digital loopback of the speaker audio currently being rendered by the DAC/amplifier. The AEC adaptive filter cross-correlates this reference with the microphone input to model the room impulse response and subtract the loudspeaker echo.
Output Pin 0 (Mono Clean Speech - 1 Channel 16 kHz S16_LE): - Feeds
mixin.105.1→kpb.106.1for keyword spotting. - Keyword spotting engines (e.g. MFCC feature extractors, DNN classifiers) require a single, normalized, echo-free mono channel. Spatial cues or stereo phase differences can degrade acoustic model recognition accuracy.Output Pin 1 (Stereo Clean Speech - 2 Channels 16 kHz S16_LE): - Feeds
mixin.105.2→host-copier.10(ALSA PCM 10) for human teleconferencing. - Preserves natural stereo spatial imaging for VoIP, meeting applications, and host-side recordings while stripping out background noise and speaker feedback.
Step 2: Implement the Component Interface#
Implement the multi-pin audio adapter in src/audio/my_ecns/my_ecns.c using the SOF processing module framework:
Module Lifecycle Architecture#
A compliant SOF processing module must implement several key lifecycle handlers:
``init()``: Allocates the component’s private context structure (
struct my_ecns_comp_data) usingmod_alloc()and initializes default algorithmic parameters.``prepare()``: Invoked before audio streaming commences. The module queries negotiated stream configurations, validates buffer parameters via
comp_verify_params(), and allocates large runtime memory buffers (e.g. AEC filter state, FFT scratch memory) usingrballoc_align()withSOF_MEM_ZONE_SYS_RUNTIMEandSOF_MEM_CAPS_RAM.``process()``: The core audio processing routine called repeatedly by the pipeline scheduler.
``reset()`` and ``free()``: Tears down the module, frees all dynamically allocated memory via
rfree(), and cleans up registered notifiers.Runtime IPC Tuning (``set_value()`` / ``get_value()``): Processes large configuration blobs sent from host user space (e.g.
sof-ctlor ALSA mixer controls) to dynamically tune filter adaptation speeds, double-talk sensitivity, and noise attenuation decibels.
Component Implementation Template#
// SPDX-License-Identifier: BSD-3-Clause
#include <sof/audio/module_adapter/module/generic.h>
#include <sof/audio/sink_api.h>
#include <sof/audio/source_api.h>
#include <sof/audio/component.h>
#include <rtos/init.h>
SOF_DEFINE_REG_UUID(my_ecns);
LOG_MODULE_REGISTER(my_ecns, CONFIG_SOF_LOG_LEVEL);
struct my_ecns_comp_data {
void *aec_state;
int period_frames; /* 320 frames for 20ms @ 16 kHz */
int16_t scratch[1280] __aligned(16);
};
static int my_ecns_init(struct processing_module *mod)
{
struct comp_dev *dev = mod->dev;
struct my_ecns_comp_data *cd;
cd = mod_alloc(sizeof(*cd));
if (!cd)
return -ENOMEM;
mod->priv_data = cd;
cd->period_frames = 320; /* 20ms period */
return 0;
}
static int my_ecns_prepare(struct processing_module *mod)
{
struct my_ecns_comp_data *cd = module_get_private_data(mod);
/* Allocate algorithm filter state cache-aligned to 16 bytes */
cd->aec_state = rballoc_align(0, SOF_MEM_CAPS_RAM,
SOF_MEM_ZONE_SYS_RUNTIME, 16,
my_aec_get_state_size());
if (!cd->aec_state)
return -ENOMEM;
my_aec_init(cd->aec_state, 16000, 4, 1);
return 0;
}
static int my_ecns_process(struct processing_module *mod,
struct sof_source **sources, int num_of_sources,
struct sof_sink **sinks, int num_of_sinks)
{
struct my_ecns_comp_data *cd = module_get_private_data(mod);
struct sof_source *src = sources[0];
struct sof_sink *sink_clean_mono = sinks[0]; /* Pin 0 -> KPB */
struct sof_sink *sink_clean_stereo = sinks[1]; /* Pin 1 -> Host */
int in_frames = source_get_data_frames_available(src);
int out_frames_0 = sink_get_free_frames(sink_clean_mono);
int out_frames_1 = sink_get_free_frames(sink_clean_stereo);
/* Determine the maximum common frames that can be safely processed */
int frames = MIN(in_frames, MIN(out_frames_0, out_frames_1));
if (frames < cd->period_frames)
return 0; /* Wait until a full 20ms period (320 frames) is available */
/* Retrieve read pointer from 4-channel input and write pointers for sinks */
int16_t *in_ptr = source_get_read_ptr(src);
int16_t *out_mono_ptr = sink_get_write_ptr(sink_clean_mono);
int16_t *out_stereo_ptr = sink_get_write_ptr(sink_clean_stereo);
/* Execute custom acoustic echo cancellation & noise suppression */
my_custom_aec_process(cd->aec_state, in_ptr, out_mono_ptr,
out_stereo_ptr, cd->period_frames);
/* Commit consumed input frames and produced output frames */
source_seek_read_ptr(src, cd->period_frames * source_get_frame_bytes(src));
sink_seek_write_ptr(sink_clean_mono, cd->period_frames * sink_get_frame_bytes(sink_clean_mono));
sink_seek_write_ptr(sink_clean_stereo, cd->period_frames * sink_get_frame_bytes(sink_clean_stereo));
return 0;
}
static int my_ecns_reset(struct processing_module *mod)
{
struct my_ecns_comp_data *cd = module_get_private_data(mod);
if (cd->aec_state) {
rfree(cd->aec_state);
cd->aec_state = NULL;
}
return 0;
}
Step 3: Update Buffer Sizing in Topology 2.0 (IBS & OBS Deep Dive)#
In ALSA Topology 2.0, two essential configuration tokens govern audio buffer scheduling and memory allocation:
``ibs`` (Input Buffer Size): Specifies the minimum number of bytes required in the component’s input buffer before the pipeline scheduler triggers the module’s
process()callback.``obs`` (Output Buffer Size): Specifies the minimum number of free bytes required in the downstream sink buffer before the module can execute, and represents the byte quantity produced by the module in a single execution period.
Why IBS and OBS are Critical to Pipeline Execution#
Topology Memory Allocation: When the SOF kernel driver parses the topology manifest, it allocates inter-component circular ring buffers sized as integer multiples of
ibsandobs(typically \(2 \times \text{ibs}\) or \(3 \times \text{ibs}\) to allow double or triple buffering). An incorrect token will allocate either insufficient memory (causing overrun) or excessive memory (wasting tightly constrained DSP SRAM).Scheduler Execution Thresholds: The SOF pipeline scheduler queries
source_get_data_available()andsink_get_free_size(). It will only dispatch the module when:\[\text{data\_available} \ge \text{ibs} \quad \text{and} \quad \text{free\_space} \ge \text{obs}\]If
ibsis mistakenly set to 128 bytes (1 ms) while the C module internally waits for 2560 bytes (20 ms), the pipeline scheduler will wake the component 20 times per period, 19 of which will immediately exit without processing data. This introduces severe CPU scheduling overhead and prevents the DSP from entering low-power sleep states.Parameter Validation Handshake: During stream startup, the firmware invokes
comp_verify_params()to cross-check that the component’s configured algorithmic frame chunk matches the topology’s declaredibsandobs. If a discrepancy exists, the driver fails with an IPC parameter error (-EINVAL).
Mathematical Calculation for Multi-Pin ECNS#
The buffer size in bytes is determined by the standard chunk formula:
For our 20 ms ECNS configuration at 16 kHz S16_LE (2 bytes per sample, 320 frames per chunk):
Input Pin 0 (4 Channels): \(16000 \times 0.020 \times 4 \times 2 = \mathbf{2560\text{ bytes}} \implies \mathbf{ibs\ 2560}\)
Output Pin 0 (Mono Clean to KPB - 1 Channel): \(16000 \times 0.020 \times 1 \times 2 = \mathbf{640\text{ bytes}} \implies \mathbf{obs\ 640}\)
Output Pin 1 (Stereo Clean to Host - 2 Channels): \(16000 \times 0.020 \times 2 \times 2 = \mathbf{1280\text{ bytes}} \implies \mathbf{obs\ 1280}\)
Topology 2.0 Widget Declaration#
In tools/topology/topology2/dmic-wov-multi-4ch-manifest.conf, declare the ECNS widget with separate output audio format blocks matching each output pin index:
Object.Widget.ecns.1 {
uuid $MY_ECNS_UUID
num_input_pins 1
num_output_pins 2
num_input_audio_formats 1
num_output_audio_formats 2
Object.Base.input_audio_format [
{
in_rate 16000
in_channels 4
in_bit_depth 16
in_valid_bit_depth 16
ibs 2560 # 20ms @ 16 kHz 4ch S16_LE
}
]
Object.Base.output_audio_format [
# Pin 0: Mono clean to KPB (output_pin_index 0)
{
output_pin_index 0
out_rate 16000
out_channels 1
out_bit_depth 16
out_valid_bit_depth 16
obs 640 # 20ms @ 16 kHz 1ch S16_LE
}
# Pin 1: Stereo clean to Host (output_pin_index 1)
{
output_pin_index 1
out_rate 16000
out_channels 2
out_bit_depth 16
out_valid_bit_depth 16
obs 1280 # 20ms @ 16 kHz 2ch S16_LE
}
]
}
—
Step-by-Step Guide: Replacing the WOV / Keyword Detector Module#
In multi-slot keyword spotting architectures, multiple detector modules run in parallel pipelines, each monitoring the clean mono speech stream for a specific trigger phrase (e.g. Slot 0 for “strawberry”, Slot 1 for “banana”, Slot 2 for “orange”). Developers can replace the reference mww (microWakeWord) or detect_test component with custom neural acoustic models, TensorFlow Lite for Microcontrollers (TFLM) models, or third-party engines (e.g. Picovoice Porcupine, Sensory TrulyHandsfree).
Step 1: Implement the Detector Interface#
A keyword detector component in SOF must handle two concurrent responsibilities: 1. Real-Time Acoustic Inference: Continuously stream mono 16 kHz audio frames (typically in 10 ms chunks = 160 frames), extract features (e.g. Mel filterbanks), and execute the neural network classifier. 2. Asynchronous Arbitration & State Synchronization: Respond to Arbiter control events (pause/resume) and trigger downstream pre-roll draining when a keyword is recognized.
Streaming Detector Architecture#
In src/audio/my_wov/my_wov.c, implement the detector module adapter:
// SPDX-License-Identifier: BSD-3-Clause
#include <sof/audio/module_adapter/module/generic.h>
#include <sof/audio/wov_arbiter.h>
#include <sof/audio/kpb.h>
#include <sof/lib/notifier.h>
SOF_DEFINE_REG_UUID(my_wov);
LOG_MODULE_REGISTER(my_wov, CONFIG_SOF_LOG_LEVEL);
struct my_wov_comp_data {
int slot_id; /* 0, 1, or 2 assigned via topology */
bool paused; /* Suppressed when a sibling slot wins */
bool detected; /* Latched true upon keyword trigger */
void *model_context; /* Neural network / TFLM runtime state */
};
/* Notifier callback: Arbiter commands this slot to PAUSE or RESUME */
static void my_wov_ctrl_cb(void *arg, enum notify_id type, void *data)
{
struct comp_dev *dev = arg;
struct my_wov_comp_data *cd = module_get_private_data(dev->mod);
struct wov_ctrl_event_data *ctrl = data;
if (ctrl->cmd == WOV_CMD_PAUSE) {
/* Sibling slot triggered: pause local inference to save DSP MCPS */
cd->paused = true;
} else if (ctrl->cmd == WOV_CMD_RESUME) {
/* Host stream reset: re-arm detector for the next trigger */
cd->paused = false;
cd->detected = false;
my_model_reset(cd->model_context);
}
}
static int my_wov_init(struct processing_module *mod)
{
struct comp_dev *dev = mod->dev;
struct my_wov_comp_data *cd;
cd = mod_alloc(sizeof(*cd));
if (!cd)
return -ENOMEM;
mod->priv_data = cd;
cd->slot_id = dev->ipc_config.index; /* Slot ID from topology widget index */
cd->paused = false;
cd->detected = false;
return 0;
}
static int my_wov_prepare(struct processing_module *mod)
{
struct comp_dev *dev = mod->dev;
struct my_wov_comp_data *cd = module_get_private_data(mod);
/* Initialize acoustic model engine and allocate scratch buffers */
cd->model_context = my_model_init();
if (!cd->model_context)
return -ENOMEM;
/* Register callback to receive PAUSE / RESUME commands from Arbiter */
notifier_register(dev, cd, NOTIFIER_ID_WOV_CTRL, my_wov_ctrl_cb, 0);
return 0;
}
static int my_wov_process(struct processing_module *mod,
struct sof_source **sources, int num_of_sources,
struct sof_sink **sinks, int num_of_sinks)
{
struct my_wov_comp_data *cd = module_get_private_data(mod);
struct sof_source *source = sources[0];
struct sof_sink *sink = sinks[0];
int frames = source_get_data_frames_available(source);
if (frames == 0)
return 0;
/* If active and not paused by arbiter, run acoustic model inference */
if (!cd->paused && !cd->detected) {
if (my_model_detect_keyword(cd->model_context, source, frames)) {
cd->detected = true;
/* 1. Command KPB to drain 2.0s pre-roll buffer to active slot */
/* 2. Inform Arbiter that this slot won detection */
my_wov_trigger_detection(mod->dev, cd->slot_id);
}
}
/* Forward audio downstream to maintain pipeline continuity */
source_to_sink_copy(source, sink, true, frames * source_get_frame_bytes(source));
return 0;
}
static int my_wov_reset(struct processing_module *mod)
{
struct comp_dev *dev = mod->dev;
struct my_wov_comp_data *cd = module_get_private_data(mod);
/* Mandatory: Unregister notifier to avoid dangling callbacks */
notifier_unregister(dev, cd, NOTIFIER_ID_WOV_CTRL);
if (cd->model_context) {
my_model_free(cd->model_context);
cd->model_context = NULL;
}
return 0;
}
Step 2: Configure Model Parameters in Topology#
To integrate the custom keyword detector into ALSA Topology 2.0, declare the widget class in tools/topology/topology2/include/components/my_wov.conf:
Understanding the Topology 2.0 Widget Definition#
Attributes: -
index: Pipeline index where the widget is instantiated. -instance: Unique instance number across all detector widgets. -cpc: Cycles Per Chunk (CPC) estimate for the scheduler’s dynamic load balancer.UUID Binding: - The
uuidstring must match the 16-byte UUID defined viaSOF_DEFINE_REG_UUID(my_wov)in the C driver.Pin Counts: - Single input pin (
num_input_pins 1) receiving 1ch clean mono speech from KPB. - Single output pin (num_output_pins 1) routing towov_arbiter.Buffer Constraints: - Sized for a 10 ms Data Processing period: \(16000 \times 0.010 \times 1 \times 2 = \mathbf{320\text{ bytes}}\) (
ibs 320,obs 320).
Widget Class Definition#
Class.Widget."my_wov" {
DefineAttribute."index" { type "integer" }
DefineAttribute."instance" { type "integer" }
DefineAttribute."cpc" { token_ref "comp.word" }
<include/components/widget-common.conf>
attributes {
!constructor [ "index", "instance" ]
!mandatory [ "uuid", "num_input_audio_formats", "num_output_audio_formats" ]
unique "instance"
}
uuid "1f:d5:a8:eb:27:78:b5:47:82:ee:de:6e:77:43:af:67"
type "effect"
no_pm "true"
num_input_pins 1
num_output_pins 1
}
In the multi-slot manifest (dmic-wov-multi-4ch-manifest.conf), instantiate the widget within each detector pipeline (Pipelines 101, 102, 103), binding the model-specific binary data blobs (such as trained neural network weight files or detection threshold structures).
—
Scheduling, Periods & Core Affinity in Topology 2.0#
Configuring a high-performance audio graph requires matching Execution Domains, Scheduling Periods, and Multi-Core Affinities:
Figure 224 Figure 323: Scheduling domains, period boundaries (1ms LL vs 10ms/20ms DP), and multi-core affinity assignments.#
Low-Latency (LL) vs. Data Processing (DP) Domains#
Low-Latency Domain (``lp_mode 0``, 1 ms period): - Synchronized directly to the hardware 1 ms timer tick. - Processes small sample chunks (16 frames at 16 kHz). - Applied to hardware DAIs (
dai-copier),wov_arbiter, and host PCMs (host-copier) to maintain minimum end-to-end latency.Data Processing Domain (``lp_mode 1``, 10 ms or 20 ms period): - Batches samples into larger algorithmic frames (160 frames for 10 ms; 320 frames for 20 ms). - Applied to compute-intensive algorithms (ECNS, MFCC, microWakeWord, TFLM). - Power Optimization: Allows the DSP core to execute heavy vector math in a brief burst and sleep in low-power idle states between chunks.
Mathematical Period & Buffer Sizing Rule#
When configuring an audio component in ALSA Topology 2.0, input buffer size (ibs) and output buffer size (obs) must satisfy the sample chunk equation:
Processing Block |
Period |
Channels |
Calculated Buffer Size (IBS / OBS) |
|---|---|---|---|
LL 1ms Capture |
1 ms |
4 channels |
\(16000 \times 0.001 \times 4 \times 2 = \mathbf{128\text{ bytes}}\) |
DP 10ms Keyword Slot |
10 ms |
1 channel (mono) |
\(16000 \times 0.010 \times 1 \times 2 = \mathbf{320\text{ bytes}}\) |
DP 20ms ECNS Input |
20 ms |
4 channels |
\(16000 \times 0.020 \times 4 \times 2 = \mathbf{2560\text{ bytes}}\) |
DP 20ms Clean Mono |
20 ms |
1 channel (mono) |
\(16000 \times 0.020 \times 1 \times 2 = \mathbf{640\text{ bytes}}\) |
DP 20ms Clean Stereo |
20 ms |
2 channels |
\(16000 \times 0.020 \times 2 \times 2 = \mathbf{1280\text{ bytes}}\) |
Warning
If the topology ibs or obs token differs from the buffer size expected by the firmware module, pipeline initialization will fail with an IPC buffer alignment error (-EINVAL) during comp_verify_params().
Cross-Core Task Affinity (core_id)#
Multi-keyword spotting workloads execute continuous STFT framing, Mel filterbank feature generation, and neural network matrix multiplications. On multi-core DSP architectures (such as Intel cAVS 2.5 on Tiger Lake and ACE on Meteor Lake / Arrow Lake / Panther Lake), running three or more concurrent keyword detectors on Core 0 can saturate the primary core, starving real-time audio copiers and causing audible buffer underruns.
Inter-Core Audio Routing Architecture#
To achieve deterministic real-time performance, SOF supports cross-core task affinity in ALSA Topology 2.0:
Core 0 (Primary Core): Runs real-time hardware copiers and preprocessing: - Pipeline 100 (DAI Capture, LL 1ms) - Pipeline 105 (ECNS Processing, DP 20ms) - Pipeline 106 (KPB History Buffer, DP 20ms) - Pipeline 101 (Slot 0 Detector, DP 10ms) - Pipeline 102 (Slot 1 Detector, DP 10ms) - Pipeline 104 (WOV Arbiter & PCM 11 Copier, LL 1ms) - Pipeline 107 (ECNS Host Copier & PCM 10, LL 1ms)
Core 1 (Secondary Core): Offloaded compute-intensive detector: - Pipeline 103 (Slot 2 Detector, DP 10ms)
SOF manages inter-core audio communication locklessly using decoupled ring buffers located in shared DSP SRAM. When Pipeline 106 on Core 0 pushes audio into its output mixin (mixin.106.1), Pipeline 103 on Core 1 reads from mixout.103.1 across DSP cache boundaries without requiring kernel mutex locks or blocking synchronization.
Configuring Core Affinity in Topology 2.0#
To assign a pipeline to a secondary core, set the core_id token in the pipeline widget definition:
# Pipeline 103: WOV Slot 2 offloaded to DSP Core 1
Object.Pipeline."custom-dp-capture" [
{
index 103
Object.Widget.pipeline.1 {
priority 0
lp_mode 1
core_id 1 # Cross-core execution on Core 1!
}
}
]
—
Building, Deploying & Verifying the Pipeline#
Compiling Topology with the NHLT Preprocessor Plugin#
The Non-HD-Audio Link Table (NHLT) is an ACPI BIOS data structure that defines microphone physical geometry, hardware PDM decimation filters, clock configurations, and vendor-specific audio endpoints.
Under normal circumstances, the SOF Linux driver parses the NHLT table supplied by the host PC’s BIOS. However, on pre-production development hardware, BIOS ACPI tables frequently lack 4-channel native 16 kHz DMIC descriptors (often providing only generic 2-channel 48 kHz tables). To resolve this, the ALSA topology compiler (alsatplg) utilizes a dedicated NHLT Preprocessor Plugin to generate and embed a validated 16 kHz 4-channel NHLT blob directly into the binary topology file:
cd $SOF_WORKSPACE/sof
TPLG2=$(pwd)/tools/topology/topology2
ALSA_TMP=/tmp/alsa-tplg-wov
mkdir -p $ALSA_TMP
cp /usr/share/alsa/alsa.conf $ALSA_TMP/
ln -sf $TPLG2/include $ALSA_TMP/include
ln -sf $TPLG2/platform $ALSA_TMP/platform
# Compile Native 16 kHz 4-Channel Multi-WOV Topology with embedded NHLT
ALSA_CONFIG_DIR=$ALSA_TMP ALSA_TOPOLOGY_PLUGIN_DIR=/usr/lib/alsa-topology alsatplg \
-I $TPLG2 -p \
-c tools/topology/topology2/dmic-wov-multi-4ch-manifest.conf \
-o /tmp/sof-tgl-dmic-wov-multi-4ch.tplg
BIOS NHLT Table Override (sof_use_tplg_nhlt=1)#
To instruct the Linux SOF kernel driver to bypass the incomplete host BIOS ACPI table and consume the NHLT table packaged inside the topology file, configure the driver module option sof_use_tplg_nhlt=1.
Add to /etc/modprobe.d/sof.conf on the target device:
options snd_sof tplg_path=intel/sof-ipc4-tplg tplg_filename=sof-tgl-dmic-wov-multi-4ch.tplg
options snd_sof_intel_hda_common sof_use_tplg_nhlt=1
Reload the audio driver stack to apply the override:
ssh root@<target_ip> '
modprobe -r snd_sof_pci_intel_tgl snd_sof_intel_hda_common snd_sof
modprobe snd_sof_intel_hda_common sof_use_tplg_nhlt=1
modprobe snd_sof_pci_intel_tgl
'
Inspect the kernel log via dmesg | grep -i nhlt to confirm successful activation:
sof-audio-pci-intel-tgl: using topology NHLT table instead of ACPI
Verifying Both Audio Streams on Target Hardware#
Step 1: Inspect ALSA Capture Endpoints#
Verify that the kernel has enumerated both host copier endpoints:
ssh root@<target_ip> 'arecord -l'
Expected output should list both PCM 10 and PCM 11:
card 0: sofhdadsp [sof-hda-dsp], device 10: ECNS Capture (*) []
Subdevices: 1/1
Subdevice #0: subdevice #0
card 0: sofhdadsp [sof-hda-dsp], device 11: WOV Capture (*) []
Subdevices: 1/1
Subdevice #0: subdevice #0
Step 2: Verify Simultaneous ECNS Teleconferencing Audio (PCM 10)#
Record a 5-second sample from PCM 10 to confirm that the ECNS module is actively processing 2-channel clean speech:
ssh root@<target_ip> 'arecord -D hw:0,10 -c 2 -r 16000 -f S16_LE -d 5 /tmp/ecns_clean.wav'
Play back /tmp/ecns_clean.wav to verify that speaker playback echo is cancelled and ambient room noise is suppressed.
Step 3: Monitor Active Keyword Slot State via ALSA Mixer#
Query the ALSA mixer control exposed by the WOV Arbiter to observe slot detection status:
ssh root@<target_ip> "amixer -c0 sget 'wov_active_slot'"
Output states:
* 0: Idle / Listening mode across all detector slots.
* 1: Slot 0 detected (“strawberry”).
* 2: Slot 1 detected (“banana”).
* 3: Slot 2 detected (“orange”).
Step 4: Capture Triggered Keyword Audio with 2.0s Pre-Roll (PCM 11)#
When speaking a trigger phrase (e.g. “strawberry”), capture the drained audio stream from PCM 11:
ssh root@<target_ip> 'arecord -D hw:0,11 -c 1 -r 16000 -f S16_LE -d 4 /tmp/wov_trigger.wav'
Verify the recorded waveform in an audio editor (e.g. Audacity or MHWaveEdit). The file should contain approximately 2.0 seconds of history buffer audio preceding the trigger point, followed seamlessly by live spoken speech.
—
Summary: Developer “Watch Out” Checklist#
Domain |
Common Failure Mode |
Correct Engineering Rule |
|---|---|---|
Multi-Pin Output |
Attempting to output both mono and stereo streams from a single output pin. |
Declare 2 output pins on ECNS: Pin 0 = 1ch mono (to KPB), Pin 1 = 2ch stereo (to Host Copier 10). |
Buffer Sizing (IBS/OBS) |
Mismatched |
Calculate exact byte size: \(\text{Rate} \times \text{Period} \times \text{Channels} \times \text{Bytes}\). |
Notifier Callbacks |
Forgetting to register for |
All detector slots must handle |
Pre-Roll Sizing |
Setting KPB history buffer depth too small in topology. |
Set |
Cross-Core Affinity |
Placing all compute-intensive neural network models on Core 0. |
Assign secondary detector slots to |
NHLT ACPI Descriptors |
DMIC failing to open natively at 16 kHz on 4 channels due to outdated BIOS ACPI table. |
Enable |