Skip to content

WIP: multi-slot WOV arbiter with VAD gate and D0I3/S0iX support - #11107

Draft
lgirdwood wants to merge 25 commits into
thesofproject:mainfrom
lgirdwood:feature/wov-multi-kpb-v2
Draft

WIP: multi-slot WOV arbiter with VAD gate and D0I3/S0iX support#11107
lgirdwood wants to merge 25 commits into
thesofproject:mainfrom
lgirdwood:feature/wov-multi-kpb-v2

Conversation

@lgirdwood

@lgirdwood lgirdwood commented Aug 20, 2026

Copy link
Copy Markdown
Member

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_arbiter drains the KPB ring buffer to the host via an ALSA compress device and
pauses 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:#555
Loading

Pipeline 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.

Host                        Kernel / ASoC               Firmware
────                        ─────────────               ────────
open comprC0D11         ──► SET_PIPELINE_STATE(RUNNING) ──► VAD: vad_active=false (threshold default=0 → open)
                                                             KPB: KPB_STATE_BUFFERING (shared ring buffer filling, all 3 slots)
                                                             detect_tests: dp_thread running, listening

[optional] write
vad_gate_cfg_100 TLV     ──► MODULE_LARGE_CONFIG_SET  ──► threshold, onset, hangover updated

write wov_init_1NN TLV   ──► MODULE_LARGE_CONFIG_SET  ──► wov_slot_id programmed (slot 0/1/2)
amixer cset wov_mute on  ──► MODULE_LARGE_CONFIG_SET  ──► detection armed for that slot

─────────────────────── Listening (blocking) ───────────────────────────
read() blocks on comprC0D11
arbiter writes silence (zero-fill) into HDA DMA fragments at realtime rate

WOV Trigger Sequence

[voice detected]                                     ──► detect_test auto/real trigger
                        ◄── SOF_IPC4_NOTIFY_PHRASE_DETECTED  (word_id = slot_id)
                        ◄── snd_sof_compr_fragment_elapsed() wakes blocked read()
read() returns PCM ◄────────────────────────────────────────
  (burst: 6 s pre-roll arrives fast, then realtime)

DSP-Initiated Re-Arm (VAD Silence Path)

This is the no-reset re-arm: compress device stays open, pipeline stays RUNNING.

[speech ends, ambient noise below threshold for hangover_frames]
                                                     ──► vad_update_energy(): energy < threshold
                                                         for hangover_frames (default 200 × 10ms = 2s)
                                                         → notifier_event(NOTIFIER_ID_VAD_SILENCE)
                                                         → arb_on_vad_silence():
                                                             active_slot = WOV_ARB_NO_ACTIVE
                                                             broadcast WOV_ARB_CMD_RESUME
                                                         → detect_tests: cd->paused=false, cd->detected=0
                                                         → KPB resumes BUFFERING (shared ring buffer, drain complete)
                                                         → DSP clock → WOVCRO (38.4 MHz)

poll vad_gate_status_100 ────────────────────────────►  MODULE_LARGE_CONFIG_GET(param_id=2)
(TLV ioctl read)                                        returns energy + vad_active=false

Host detects silence, drains remaining compress data
read() blocks again on comprC0D11 ◄───────────────── arbiter back to silence-fill (zero frames)
─────────────────────── Re-armed, listening ─────────────────────────────────

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 and
polling the vad_gate_status_100 kcontrol:

[keyword audio delivered; speech is fading]
                                                     ──► vad_gate energy drops below threshold
                                                         → NOTIFIER_ID_VAD_SILENCE
                                                         → arb_on_vad_silence():
                                                             active_slot = WOV_ARB_NO_ACTIVE
                                                             broadcast WOV_ARB_CMD_RESUME
                                                         → detect_tests resume DP threads

poll vad_gate_status_100 ──► MODULE_LARGE_CONFIG_GET ──► vad_active=false confirmed

[host: drain remaining compress data — read until 600 ms idle]
read() blocks on comprC0D11 ◄───────────────────────── arbiter back to silence-fill
─────────────────────── Re-armed, same compress handle ──────────────────────────

No compress_stop/start between cycles. compress_stop() is issued only when the
application exits (or the pipeline is torn down). Calling compress_stop() between
cycles is unnecessary and resets DMA state, adding latency to the next trigger.

State Transition Summary

State Transition Trigger Compress device
Listening → Active WOV_DETECT detect_test fires Stays OPEN
Active → Listening VAD_SILENCE + drain + kcontrol poll hangover expires, host drains Stays OPEN
Active → Listening STOP/PAUSE pipeline teardown / app exit Closed on compress_close()
Any → closed compress_close() host exits Closed

SOF Notifier Inter-Module Signaling

The SOF Notifier system (src/include/sof/lib/notifier.h) is SOF's intra-DSP
publish/subscribe bus. It works on all platforms (no CONFIG_AMS required) and
is already used for KPB client events. Signals are delivered synchronously to
all registered listeners on the calling core.

Signal Catalog

Notifier ID Direction Payload struct Purpose
NOTIFIER_ID_WOV_DETECT detector → arbiter struct wov_detect_notif { uint8_t slot_id; } Announce keyword detection
NOTIFIER_ID_WOV_CTRL arbiter → all detectors struct wov_ctrl_notif { uint8_t cmd; } Pause/resume detectors
NOTIFIER_ID_VAD_SILENCE vad_gate → arbiter NULL (no payload) Silence hangover expired — re-arm for next trigger

cmd values: WOV_ARB_CMD_PAUSE, WOV_ARB_CMD_RESUME (defined in wov_arbiter.h).

NOTIFIER_ID_VAD_SILENCE is fired by vad_update_energy() when the IIR energy drops
below threshold for hangover_frames consecutive frames. The arbiter's
arb_on_vad_silence() callback handles it: resets active_slot = WOV_ARB_NO_ACTIVE
and broadcasts WOV_ARB_CMD_RESUME to all detectors so they re-arm without any
pipeline 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
Loading

@lgirdwood
lgirdwood force-pushed the feature/wov-multi-kpb-v2 branch 4 times, most recently from 653a1b9 to b656712 Compare August 26, 2026 14:54
@lgirdwood

Copy link
Copy Markdown
Member Author

@gkdeepa @naveen-manohar fyi

@lgirdwood
lgirdwood force-pushed the feature/wov-multi-kpb-v2 branch from 2a1c8ea to 5017c6c Compare September 3, 2026 20:18
…ynamic sample rates

- Add downstream WOV detector client notification and buffer drainage
- Support configurable history buffer depth
- Add dynamic sample rate calculations supporting both 16kHz and 48kHz

Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
- Add multi-slot WOV arbiter component
- Support mono-to-stereo channel expansion and sample width casting
- Implement period-bounded zero-fill pacing during idle/startup
- Add single-source default slot 0 routing

Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
@lgirdwood
lgirdwood force-pushed the feature/wov-multi-kpb-v2 branch from 2a3cfc8 to 89bba02 Compare September 6, 2026 18:34
- Add multi-slot WOV test framework with DP thread scheduling
- Add per-slot wov_mute switch kcontrol
- Update sink buffer pacing and threshold calculation for bench test

Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
…anifests

- Add dmic-wov-multi-manifest.conf: 48kHz 4ch DAI copier matching hardware NHLT
  with in-DSP SRC downsampler (48kHz -> 16kHz), 16kHz KPB, dual WOV detectors,
  and regular ALSA PCM capture via host-copier (hw:0,11)
- Add multi-slot WOV manifests for HDA and SSP0/1/2
- Add regular stereo (dmic-regular-manifest.conf) and dual stereo
  (dmic-dual-regular-manifest.conf) 48kHz DMIC capture manifests
- Add widget and pipeline definitions for KPB and WOV arbiter
- Add capture_compatible_d0i3 support to host-copier

Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
…suite

wov_capture_app: self-contained zero-dependency C host application
using direct Linux ALSA kernel UAPI ioctls (/dev/snd/pcmC%uD%uc).
- Dynamic HW/SW parameter negotiation adapting to driver-selected periods
- Signal metrics calculation (DC offset, AC RMS, Peak amplitude, Peak dBFS)
- Valid RIFF WAV file generation with precise byte headers

wov_daemon.py: Python multi-cycle test runner with DSP runtime PM
D3 autosuspend / wake cycle verification.

Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
@lgirdwood
lgirdwood force-pushed the feature/wov-multi-kpb-v2 branch from 89bba02 to 536d447 Compare September 6, 2026 18:58
lrgirdwo and others added 13 commits September 7, 2026 10:49
- Add dmic-wov-multi-ptl-manifest.conf and dmic-wov-multi-wcl-manifest.conf
  matching the TGL multi-slot WOV architecture (48kHz 4ch DAI copier,
  in-DSP SRC downsampler 48kHz -> 16kHz, 16kHz KPB, 3 WOV detector slots,
  and regular ALSA PCM capture via host-copier on hw:0,11).
- Add platform/intel/wcl.conf for Wildcat Lake with DMIC_DRIVER_VERSION 5
  and SSP_BLOB_VERSION 0x300.
- Add sof-ptl-dmic-wov-multi and sof-wcl-dmic-wov-multi targets to
  production/tplg-targets-ace3.cmake.
- Add wcl platform support to sof-hda-generic-wov-manifest.conf.

Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
…for TGL, PTL, WCL

- Add dmic-wov-multi-4ch-manifest.conf (TGL), dmic-wov-multi-ptl-4ch-manifest.conf (PTL),
  and dmic-wov-multi-wcl-4ch-manifest.conf (WCL) opening DMIC directly at 16kHz with 4
  channels without SRC downsampler.
- Add embedded 4-channel 16kHz DMIC NHLT configuration tables via alsatplg nhlt plugin
  to override buggy BIOS NHLT tables on DUTs.
- Register sof-tgl-dmic-wov-multi-4ch in tplg-targets-cavs25.cmake, and
  sof-ptl-dmic-wov-multi-4ch / sof-wcl-dmic-wov-multi-4ch in tplg-targets-ace3.cmake.
- Update detect_test component in DSP firmware to support 4-channel audio streams and
  multi-channel DP buffer downmixing.
- Update wov_multi_slot_test.py to support configurable channel count (-c 4).
- Verified on Spider DUT with 100% pass across all multi-slot trigger cycles and audio captures.

Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
- Document embedded NHLT table generation via alsatplg nhlt preprocessor
  plugin (PREPROCESS_PLUGINS "nhlt") for native 16kHz 4-channel DMIC.
- Document Linux SOF kernel driver BIOS NHLT override mechanism
  (snd_sof_intel_hda_common.sof_use_tplg_nhlt=1).
- Add DUT modprobe reload and verification instructions to wov_arbiter
  and topology2 READMEs.

Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
Add missing components to WCL modules overlay:
CONFIG_COMP_TDFB=m
CONFIG_COMP_VOLUME=m

Signed-off-by: Naveen Manohar <naveen.m@intel.com>
Commit "topology: kpb: add topology-configurable history"
added IncludeByKey.KPB_BUFF_TIME_MS to kpb widget class but
only defined KPB_BUFF_TIME_MS in dmic-wov-multi-manifest.cf

All topologies that includes kpb.conf without manifest fails to
build. Add a "none" default to common_definitions.conf.

Log Before:
  No variable defined for KPB_BUFF_TIME_MS
  Failed to process includes
  Failed to process conditional includes in input config
  FAILED: [code=1] .../sof-dmic-4ch-wov.tplg

After:
  [0/2] Generating sof-dmic-4ch-wov.tplg

Signed-off-by: Naveen Manohar <naveen.m@intel.com>
Add a standalone feature manifest (dmic-wov-feature.conf)
for the WoV/KPB pipeline, loaded on top of the function
topology via feature_topologies.

Add single platform-agnostic feature topology target,
sof-dmic-4ch-wov, in tplg-targets-ace3.cmake so it can be
shared across PTL (rt721/rt722) and WCL.

Signed-off-by: Naveen Manohar <naveen.m@intel.com>
WCL RVP's UEFI BIOS for ALOS publishes no NHLT ACPI table,
so DSP has no DMIC hardware conf & PCM 11 failed hw_params
with EINVAL for every format.
Use topology-embedded NHLT override instead, which kernel
already accepts via sof_use_tplg_nhlt=1.

3 fixes are needed to make WCL 4-ch multi-slot WoV topology
load on this platform:

- Wire wov_arbiter.toml into WCL rimage config. The module
  & TOML were added upstream but never included by any plt,
  so FW manifest carried no WOVARB entry with kernel log:
  "failed to find module info for widget wov-arbiter.104.1".

- Enable CONFIG_COMP_WOV_ARBITER defaults to n, disable
  CONFIG_COMP_GOOGLE_HOTWORD_DETECT which needs proprietary
  hotword_dsp_api.h and has no ACE30 library build.

- Retarget manifest from dmic01->dmic16k. dmic01 is already
  claimed by base topology sof-wcl-dmic-4ch-id5.tplg and a
  feature topology cannot rebind a bound BE; attempting it
  failed snd_soc_component_probe().

- Reduce KPB_BUFF_TIME_MS from 6000 to 2100, as 6000 ms of
  4ch/32-bit/16 kHz history is 1536000B exceeds DSP heap.

PCM11 now enumerates as "DMIC Multi-WOV 4CH", topology loads
without error & hw_params/prepare succeed.

Signed-off-by: Naveen Manohar <naveen.m@intel.com>
ecns.toml exists (added upstream) but was only included for PTL and TGL in
tools/rimage/config/*.toml.h, not WCL. CONFIG_COMP_ECNS also defaults to n.
Building WCL with the module compiled in but its TOML unregistered fails at
compile time:

  error: '_UUIDREG_ecns' undeclared here (not in a function)
  #define SOF_REG_UUID(name) _UUIDREG_##name
  ...
  SOF_DEFINE_REG_UUID(ecns);

_UUIDREG_ecns is generated from src/audio/ecns/ecns.toml via
scripts/gen-uuid-reg.py against sof/uuid-registry.txt, which already has an
ecns entry; the include was simply missing for this platform, same gap as
wov_arbiter fixed earlier for WCL.

Wire ecns.toml into wcl.toml.h under CONFIG_COMP_ECNS and enable the Kconfig
for WCL. Build now succeeds; the firmware manifest carries:

  name = "ECNS"
  uuid = "214f8a6c-493a-4b1e-8b1e-0d3b6f8a2c15"

Signed-off-by: Naveen Manohar <naveen.m@intel.com>
Add ECNS processing pipeline to WCL multi-slot WoV-topology
Route mono ECNS output through 2100 ms KPB & WoV arbiter to
PCM 11 and expose the stereo ECNS output on PCM 12.

Keep WCL NHLT-override DMIC configuration & 3 WoV detector
routes. Post-ECNS mono 16-bit KPB buffer requires 67.2kb.

Signed-off-by: Naveen Manohar <naveen.m@intel.com>
lrgirdwo and others added 3 commits September 8, 2026 13:00
…scoring kcontrols

- Integrate microWakeWord (MWW) from PR 11135 to replace test WOV modules.
- Single unified codebase across 3 concurrent keyword spotters:
  * Slot 0: strawberry
  * Slot 1: banana
  * Slot 2: orange
- Add 2 Hz read-only volatile enum scoring kcontrol per MWW instance
  (MWW 101 Score, MWW 102 Score, MWW 103 Score) with IPC4 notifications.
- Unify multi-slot topology across PTL, TGL, and WCL targets.
- Verified on Aphid DUT (PTL): all modules loaded in DAPM, dynamic
  power transitions confirmed, and real-time score updates validated.
Patch for changes:
- Enable WCL MWW prerequisites and memory budget.
- Register mww.toml in the WCL rimage configuration.
- Remove stale TFLM source entries absent from the required dependency tree.
- Build strawberry, banana, and orange fruit models.
- Generate & deploy MWW UUID module:
  4067FE1D-CD63-4877-966B-A06DED1719CE.bin

Signed-off-by: Naveen Manohar <naveen.m@intel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants