WIP: multi-slot WOV arbiter with VAD gate and D0I3/S0iX support - #11107
Draft
lgirdwood wants to merge 10 commits into
Draft
WIP: multi-slot WOV arbiter with VAD gate and D0I3/S0iX support#11107lgirdwood wants to merge 10 commits into
lgirdwood wants to merge 10 commits into
Conversation
lgirdwood
force-pushed
the
feature/wov-multi-kpb-v2
branch
3 times, most recently
from
August 26, 2026 14:00
7240b8d to
653a1b9
Compare
Document the multi-slot Wake-on-Voice pipeline architecture, component
interactions, kcontrol interface, tools usage, and pipeline lifecycle.
Sections:
- Architecture overview: single shared KPB at pipeline 100 feeding
Mixin → 3×(Mixout → WOV → Arbiter); VAD gate gates the entire chain
- Signal catalog: SOF notifier events (WOV_DETECT, WOV_CTRL, VAD_SILENCE)
with payload types and usage notes
- Arbiter state machine: Idle → Active (keyword detect) → Idle (stream
close or VAD silence re-arm); DSP-initiated re-arm without pipeline RESET
- kcontrol reference: complete numid table for vad_gate_cfg/status,
kpb_cfg, wov_init/mute/trigger_id; amixer command examples
- Pipeline state transitions: compress device open-once flow, WOV trigger
sequence, DSP-initiated re-arm (VAD silence path), host-initiated re-arm
- VAD calibration guide: vad_calibrate.py usage, TGL noise floor table,
threshold selection for lab vs production
- Tool usage: wov_daemon.py and wov_capture_app operational guide
KPB topology: documents single-KPB design (192 KB history buffer);
KPB_BUFF_TIME_MS configurable depth (fallback to CONFIG_KPB_MAX_BUFF_TIME).
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Allow multiple downstream WOV detector pipelines to share one KPB instance by extending the KPB to support a sel_sink drain path alongside the existing dedicated host_sink path. Key changes: - Add sel_sink field to kpb_data to track the downstream WOV detector sink; kpb_set_sink() assigns it from the component bind call. - kpb_init_draining: when host_sink is NULL (multi-KPB WOV topology with no dedicated PCM capture), redirect the pre-roll drain through sel_sink so that history reaches the wov_arbiter and onward to the host copier. Initialise host_period_size from sel_sink stream geometry when not already set. Skip pausing the selector component when sel_sink is the active drain path. - kpb_init_draining: cap drain_req to the actual buffered amount instead of aborting when less history is available than requested (partial pre-roll is better than none). - kpb_reset: add immediate-reset path when host_sink==NULL in BUFFERING/DRAINING state; the LL scheduler is gone after STOP so the async-EBUSY path can never complete. - kpb_init_draining: guard against NULL host_sink to prevent NULL deref on WOV-only instances. - Fix fallback sink assignment in kpb_copy: use && instead of || so the fallback only fires when BOTH sel_sink and host_sink are NULL. - Raise KPB_MAX_BUFF_TIME to 6000 ms; clear HOST_WAKEUP_TIME (no extra delay needed with direct sel_sink routing). - kpb.conf: expose host_sink_index parameter for topology binding. - Reduce log noise: demote two comp_err to comp_dbg in the RUN copy path. Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Add three notifier event IDs for the multi-slot WOV arbitration pipeline:
NOTIFIER_ID_WOV_DETECT — fired by a keyword detector when a keyword
is confirmed; payload is struct wov_detect_notif carrying the slot_id
of the winning detector. Received by wov_arbiter to activate the
correct slot and pause all other detectors.
NOTIFIER_ID_WOV_CTRL — fired by wov_arbiter to all registered
detectors; payload is struct wov_ctrl_notif carrying a cmd (PAUSE or
RESUME) and the active_slot (WOV_ARB_NO_ACTIVE=0xff on RESUME to
address all slots).
NOTIFIER_ID_VAD_SILENCE — fired by vad_gate when the IIR energy stays
below the configured threshold for hangover_frames consecutive frames
(sustained silence). Received by wov_arbiter to reset active_slot and
broadcast RESUME without a pipeline RESET, enabling multi-cycle capture
with the compress device held open throughout.
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Add a lightweight VAD gate component between the DAI copier and the KPB
in the WOV capture pipeline (pipeline 100). It measures per-frame signal
energy via a first-order IIR estimator and controls downstream pipeline
activity:
Energy gate:
When smoothed energy falls below the configurable threshold for
hangover_frames consecutive frames, emit PPL_STATUS_PATH_STOP so the
mixin (and all downstream KPB/WOV pipelines) can idle. When energy
rises above the threshold for onset_frames frames, resume data flow.
Clock scaling:
WOVCRO (38.4 MHz) during silence; HPRO (~400 MHz) during speech.
vad_gate_reset() restores WOVCRO so a pipeline RESET during HPRO does
not leave the clock stuck at high frequency.
S16LE support:
vad_update_energy() handles both S16LE and S32LE (frame_bytes == 2 or
4) so the same component works on all platforms.
IPC4 kcontrol interface:
- vad_gate_cfg_<N> (bytes TLV, R/W): threshold, onset_frames,
hangover_frames, energy_shift written at pipeline open.
- vad_gate_status_<N> (bytes TLV, RO): volatile energy + vad_active
reported via GET_LARGE_CONFIG (param_id=2).
VAD silence notifier:
When the hangover expires, emit NOTIFIER_ID_VAD_SILENCE so wov_arbiter
can reset active_slot and re-arm all detectors without a pipeline RESET.
Kconfig: CONFIG_COMP_VAD_GATE (depends on IPC_MAJOR_4).
UUID: 5f6e7d8c-3b4a-1d2c-0e9f-8a7b6c5d4e3f
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Add the wov_arbiter audio component which manages keyword detection slot arbitration for the multi-slot WOV capture pipeline. Slot arbitration: Listens for NOTIFIER_ID_WOV_DETECT from any of up to WOV_ARB_MAX_SLOTS (3) detect_test instances. First-wins: on the first detect event, wov_arbiter activates that slot (active_slot = slot_id) and broadcasts NOTIFIER_ID_WOV_CTRL PAUSE to all other detectors via the SOF notifier bus. Audio from the active KPB pre-roll + live is forwarded to the single host-copier downstream. Before any keyword fires, the host copier sink is filled with silence. No-reset re-arm: Subscribes to NOTIFIER_ID_VAD_SILENCE in wov_arb_prepare(); when fired by vad_gate on sustained silence, arb_on_vad_silence() resets active_slot to WOV_ARB_NO_ACTIVE and broadcasts WOV_ARB_CMD_RESUME so all detectors re-arm. The compress device stays open throughout — no pipeline close/reopen required for multi-cycle capture. Debug: IPC4 SET_LARGE_CONFIG param_id=1 (IPC4_WOV_ARB_SET_ACTIVE_SLOT) forces a slot active without a keyword event (lab testing). Kconfig: CONFIG_COMP_WOV_ARBITER. UUID: 4a5b6c7d-8e9f-4a1b-2c3d-4e5f60718293 Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Extend detect_test (the sample keyword detector) to participate in the multi-slot WOV arbitration system coordinated by wov_arbiter. Slot assignment: A static wov_slot_id is derived from the pipeline ID at component creation time (pipeline 101 → slot 0, 102 → slot 1, 103 → slot 2). The slot ID can be overridden via IPC4 LARGE_CONFIG_SET param_id=4 (IPC4_DETECT_TEST_SET_WOV_SLOT). DP thread batching: Each slot runs a Zephyr k_thread at K_PRIO_PREEMPT(12) with a 4096-byte stack. The LL copy path accumulates 320-frame (20 ms at 16 kHz) S16_LE samples into a double-buffer and gives a semaphore when a batch is ready. The DP thread wakes, runs the energy detector, and signals the LL thread to switch sides. Slot 2 is pinned to DSP Core 1. Threads are started in prepare() and stopped in reset() and free(). Notifier integration: On detection, sends SOF_IPC4_NOTIFY_PHRASE_DETECTED IPC4 notification to the host (word_id = wov_slot_id) and fires NOTIFIER_ID_WOV_DETECT to wov_arbiter. Subscribes to NOTIFIER_ID_WOV_CTRL; responds to PAUSE (stops detecting) and RESUME (resets cd->detected, resumes). Per-slot mute switch: wov_mute_<N> kcontrol (SOF_IPC4_SWITCH_CONTROL_PARAM_ID): when set to 0 (muted), test_keyword_copy drains the source buffer and returns without running detect_func — keeps LL scheduler running without burning cycles for detection. Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Enable CONFIG_COMP_WOV_ARBITER, CONFIG_COMP_VAD_GATE, CONFIG_COMP_KPB,
CONFIG_SAMPLES, and CONFIG_SAMPLE_KEYPHRASE on the intel_adsp_cavs25
(TigerLake CAVS2.5) board for default WOV firmware builds.
Also advertise raw PCM as a supported compress-capture codec so the
kernel's ALSA compress core accepts the PCM stream opened on comprC0D11:
- src/audio/module_adapter/Kconfig: add CONFIG_SOF_COMPRESS_CODEC_PCM_CAP
- src/audio/base_fw.c: gate SND_COMPRESS_PT_CODEC_ID + codec_id == 0
accept on SOF_COMPRESS_CODEC_PCM_CAP.
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
… capture
Add topology files for the multi-slot Wake-on-Voice pipeline running on
TGL/CAVS2.5 and ARL-S platforms.
Architecture (single shared KPB design):
DAI copier → VAD Gate → KPB → Mixin → 3×(Mixout → WOV → Arbiter)
→ Host copier (compress capture)
A single shared 192 KB KPB buffer (vs 3 × 192 KB per-slot) serves all
three detect_test keyword detectors fanned out through the Mixin.
Components added:
- vad-gate.conf: widget class with bytes kcontrols for runtime threshold
tuning (vad_gate_cfg_<N> R/W, vad_gate_status_<N> RO energy readback)
- wov-arbiter.conf: widget class with wov_trigger_id enum kcontrol
(selects which slot routes audio to the host) and wov_mute switch
- wov.conf: per-slot wov_mute switch kcontrol (mute/unmute individual
detect_test instances without a pipeline RESET)
- host-copier.conf: capture_compatible_d0i3 attribute for D0I3/S0iX
WOV support while the DSP is in low-power state
Topology manifests:
- dmic-wov-multi-manifest.conf: HDA DMIC + topology2 manifest for TGL;
single KPB at pipeline 100, wov_trigger_id for slot routing, D0i3,
ALSA compress PCM capture (comprC0D11)
- platform/intel/dmic-wov-multi.conf: DMIC platform file reference
design (single KPB, same pipeline structure)
- sof-hda-generic-wov-manifest.conf: full HDA topology with WOV variant
Pipeline support:
- dai-mixin-be.conf: DAI-to-Mixin pipeline class (extended for inline
extra widgets like vad-gate)
- wov-kpb-be.conf: WOV slot pipeline class (Mixout + WOV, no per-slot KPB)
- wov-detect.conf: WOV detection pipeline wrapper
rimage: tgl.toml.h: add manifest segment for WOV firmware image
Tested: boots and captures on spider (TGL/CAVS2.5), 3-slot WOV with
multi-cycle re-arm verified. Single-KPB consolidation requires
re-test on spider after DUT power-on.
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Add a bytes kcontrol kpb_cfg_<N> to each KPB widget so the topology
manifest can set the ring-buffer pre-roll duration at load time without
requiring a firmware rebuild.
Firmware (IPC4 path):
- ipc4/kpb.h: add KP_BUF_CFG_BUFF_TIME_MS = 2 to the param-ID enum
- kpb.c: add buff_time_ms to comp_data; kpb_get_buff_time_ms() returns
the topology value when set, otherwise falls back to
CONFIG_KPB_MAX_BUFF_TIME. Wire the helper into kpb_params(),
kpb_prepare() buffer allocation, and drain-request cap. Handle the
new LARGE_CONFIG_SET case in kpb_set_large_config().
Topology:
- kpb.conf: add Object.Control.bytes kpb_cfg_$index with IncludeByKey
blobs for 6000/4000/2100 ms (sof_abi_hdr + u32 ms = 36 bytes total)
- dmic-wov-multi-manifest.conf: Define KPB_BUFF_TIME_MS 6000 for
TGL/CAVS2.5 (6 s pre-roll); omitting the define leaves buff_time_ms=0
and kpb falls back to CONFIG_KPB_MAX_BUFF_TIME.
The kernel sof_ipc4_widget_kcontrol_setup() sends the blob as a
LARGE_CONFIG_SET on pipeline open — no userspace intervention required.
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Add three tools for multi-cycle WOV capture testing:
vad_calibrate.py:
Runtime VAD gate threshold configuration and energy monitoring.
Reads vad_gate_cfg_100 / vad_gate_status_100 kcontrols (numid=8/9)
to set the silence threshold and display live IIR energy + vad_active
state. Prints a calibration table (noise floor, suggested threshold).
wov_daemon.py:
Python multi-cycle WOV capture daemon. Keeps comprC0D11 open across
all cycles (no pipeline close/reopen per trigger). Uses per-cycle
temp files instead of stdout pipe for robustness when the compress
device is busy. Polls vad_gate_status_100 to detect silence and drain
the KPB before re-arming.
wov_capture_app (C daemon):
Tinycompress-based C application for multi-cycle WOV capture.
- Holds comprC0D11 open across cycles; compress_wait() drives the
read/poll loop.
- Each WOV trigger writes wov_YYYYMMDD_HHMMSS_NNN.wav (S32LE 16kHz mono).
- Polls vad_gate_status_100 every 200 ms; logs energy/state changes
with [CTL] prefix and pipeline transitions with [STATE] prefix.
- Optional -t N flag writes VAD threshold at startup.
- Build: cd tools/wov_capture && make
- Deploy: make install DUT=root@spider
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
lgirdwood
force-pushed
the
feature/wov-multi-kpb-v2
branch
from
August 26, 2026 14:54
653a1b9 to
b656712
Compare
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Multi-Slot Wake-On-Voice (WOV) Architecture & Arbitration
Overview
The Multi-Slot WOV subsystem lets a single DMIC feed up to 3 concurrent keyword detectors
running on the DSP. A single shared KPB sits between the VAD gate and the fan-out mixin,
recording a pre-roll window (6 seconds on TigerLake, 2.1 seconds on other platforms) for all
three slots in one 192 KB ring buffer — versus 576 KB in a per-slot design. When any detector
fires the
wov_arbiterdrains the KPB ring buffer to the host via an ALSA compress device andpauses the other detectors. When the host closes the stream the arbiter resumes all detectors.
The ALSA compress framework (
snd_compr) decouples HDA DMA from the audio sample-rate clock.During the pre-roll burst the KPB draining EDF task fills HDA DMA fragments as fast as the bus allows
rather than at the rate-locked 16 kHz × sample-size pace of a regular PCM device.
After the pre-roll drains the live DMIC stream continues over the same compress device at realtime rate.
This design keeps host DMA live and eliminates the wakeup latency normally incurred by starting
DMA after detection.
System Architecture
Component Graph
graph TD subgraph P100["Pipeline 100 — Capture, Gating & Pre-Roll (Core 0)"] DAI["DAI Copier\nHDA Analog\ndai_index=1"] VAD["vad_gate\n(energy estimator)"] KPB["kpb\n**shared** 192 KB ring buffer\n(6 s pre-roll for all slots)"] MIX["mixin\n(1→3 fan-out)"] DAI --> VAD --> KPB --> MIX end subgraph P101["Pipeline 101 — Slot 0 (Core 0)"] MO0["mixout 0"] D0["detect_test\nSlot 0\n(Male 80–170 Hz)"] MO0 --> D0 end subgraph P102["Pipeline 102 — Slot 1 (Core 0)"] MO1["mixout 1"] D1["detect_test\nSlot 1\n(Female 175–270 Hz)"] MO1 --> D1 end subgraph P103["Pipeline 103 — Slot 2 (Core 1)"] MO2["mixout 2"] D2["detect_test\nSlot 2\n(Child 275–500 Hz)"] MO2 --> D2 end subgraph P104["Pipeline 104 — Arbitration & Host Capture (Core 0)"] ARB["wov_arbiter\n(first-wins)"] HC["host-copier\ncomprC0D11\n'DMIC Multi-WOV'"] ARB --> HC end MIX --> MO0 MIX --> MO1 MIX --> MO2 D0 --> ARB D1 --> ARB D2 --> ARB D0 -- "Notifier WOV_DETECT\n(slot_id=0)" --> ARB D1 -- "Notifier WOV_DETECT\n(slot_id=1)" --> ARB D2 -- "Notifier WOV_DETECT\n(slot_id=2)" --> ARB ARB -- "Notifier WOV_CTRL\n(PAUSE/RESUME)" --> D0 ARB -- "Notifier WOV_CTRL\n(PAUSE/RESUME)" --> D1 ARB -- "Notifier WOV_CTRL\n(PAUSE/RESUME)" --> D2 %% Row layout hints (invisible ~~~ edges — dagre rank control) %% Row 0: P100 | Row 1: P101 / P102 / P103 | Row 2: P104 KPB ~~~ MO0 MO0 ~~~ ARB style VAD fill:#2d5a27,stroke:#555 style KPB fill:#4a3a00,stroke:#555 style ARB fill:#1c4966,stroke:#555 style D0 fill:#663300,stroke:#555 style D1 fill:#660033,stroke:#555 style D2 fill:#003366,stroke:#555Pipeline State Transitions & Re-Arm Cycle
Usage Flow (Compress Device Open Once)
The compress device is opened once at startup and kept open across all WOV cycles.
No pipeline close/reopen is needed between triggers.
WOV Trigger Sequence
DSP-Initiated Re-Arm (VAD Silence Path)
This is the no-reset re-arm: compress device stays open, pipeline stays RUNNING.
Between-Cycle Re-Arm (Drain + VAD Gate Kcontrol)
The compress device stays RUNNING across all WOV cycles — no
compress_stop()/compress_start()is issued between triggers. The host re-arms by draining andpolling the
vad_gate_status_100kcontrol:State Transition Summary
SOF Notifier Inter-Module Signaling
The SOF Notifier system (
src/include/sof/lib/notifier.h) is SOF's intra-DSPpublish/subscribe bus. It works on all platforms (no
CONFIG_AMSrequired) andis already used for KPB client events. Signals are delivered synchronously to
all registered listeners on the calling core.
Signal Catalog
NOTIFIER_ID_WOV_DETECTstruct wov_detect_notif { uint8_t slot_id; }NOTIFIER_ID_WOV_CTRLstruct wov_ctrl_notif { uint8_t cmd; }NOTIFIER_ID_VAD_SILENCENULL(no payload)cmdvalues:WOV_ARB_CMD_PAUSE,WOV_ARB_CMD_RESUME(defined inwov_arbiter.h).NOTIFIER_ID_VAD_SILENCEis fired byvad_update_energy()when the IIR energy dropsbelow threshold for
hangover_framesconsecutive frames. The arbiter'sarb_on_vad_silence()callback handles it: resetsactive_slot = WOV_ARB_NO_ACTIVEand broadcasts
WOV_ARB_CMD_RESUMEto all detectors so they re-arm without anypipeline RESET or compress device close/reopen.
Full Detect-to-Drain Sequence
sequenceDiagram autonumber participant DMIC as DMIC (HW) participant KPB as KPB (shared P100) participant DET as detect_test (slot N) participant ARB as wov_arbiter participant HOST as Host Compress (comprC0D11) participant OTHER as detect_test (slots ≠ N) Note over DMIC,OTHER: Listening state — shared KPB accumulating pre-roll for all slots loop Every 1 ms (LL period) DMIC->>KPB: DAI DMA frames KPB->>DET: sel_sink copy end loop Every 20 ms (DP batch) DET->>DET: run algorithm on 320-frame batch end Note over DET,ARB: Keyword detected on slot N DET->>HOST: ① IPC4 SOF_IPC4_NOTIFY_PHRASE_DETECTED\n (word_id = slot_id) DET->>KPB: ② notifier_event(WOV_DETECT) [KPB already wired via kpb_client] DET->>ARB: ③ notifier_event(NOTIFIER_ID_WOV_DETECT, slot_id=N) ARB->>ARB: active_slot = N ARB->>OTHER: notifier_event(NOTIFIER_ID_WOV_CTRL, cmd=PAUSE) OTHER->>OTHER: cd->paused = true\n(stops DP batching) KPB->>ARB: stream pre-roll (up to 6 s) via host_sink ARB->>HOST: route slot-N audio to host PCM Note over HOST,ARB: Host finishes reading / closes PCM HOST->>ARB: trigger STOP (ALSA hw_free / snd_pcm_close) ARB->>ARB: active_slot = NO_ACTIVE ARB->>OTHER: notifier_event(NOTIFIER_ID_WOV_CTRL, cmd=RESUME) ARB->>DET: notifier_event(NOTIFIER_ID_WOV_CTRL, cmd=RESUME) OTHER->>OTHER: cd->paused = false\ncd->detected = 0\nresumed listening