From b96f61d3061b91a45181a039651a39ab896584cb Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:41:16 +0100 Subject: [PATCH 01/11] audio: kpb: add multi-client downstream WOV detector triggering 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 --- src/audio/kpb.c | 159 +++++++++++++++--- src/audio/kpb.toml | 2 +- src/include/sof/audio/kpb.h | 5 +- .../topology2/include/components/kpb.conf | 4 +- 4 files changed, 144 insertions(+), 26 deletions(-) diff --git a/src/audio/kpb.c b/src/audio/kpb.c index 2cec91393517..8bb0100c4e76 100644 --- a/src/audio/kpb.c +++ b/src/audio/kpb.c @@ -18,10 +18,14 @@ #include #include #include +#define SOF_MODULE_API_PRIVATE +#include +#include #include #include #include #include +#include #include #include #include @@ -369,6 +373,9 @@ static int kpb_bind(struct comp_dev *dev, struct bind_info *bind_data) sink_buf_id = buf_get_id(sink); if (sink_buf_id == buf_id) { + struct comp_dev *sc = comp_buffer_get_sink_component(sink); + comp_err(dev, "kpb_bind: buf_id=%d sink_comp=0x%x -> %s", + buf_id, sc ? dev_comp_id(sc) : 0, sink_buf_id == 0 ? "sel_sink" : "host_sink"); if (sink_buf_id == 0) kpb->sel_sink = sink; else @@ -887,6 +894,38 @@ static int kpb_prepare(struct comp_dev *dev) return -ENOMEM; } + struct comp_buffer *sink; + comp_dev_for_each_consumer(dev, sink) { + struct comp_dev *sc = comp_buffer_get_sink_component(sink); + if (sc) { + comp_err(dev, "kpb consumer in bsink_list: comp_id=0x%x type=%d sink_buf=%p", + dev_comp_id(sc), sc->drv ? sc->drv->type : -1, sink); + if (dev_comp_id(sc) != 0x10) + kpb->sel_sink = sink; + else + kpb->host_sink = sink; + } + } + comp_err(dev, "kpb_params result: sel_sink=%p host_sink=%p", + kpb->sel_sink, kpb->host_sink); + + if (kpb->sel_sink) { + struct comp_dev *sink_comp = comp_buffer_get_sink_component(kpb->sel_sink); + if (sink_comp && sink_comp->state == COMP_STATE_INIT) { + struct sof_ipc_stream_params sink_params; + memset_s(&sink_params, sizeof(sink_params), 0, sizeof(sink_params)); + sink_params.channels = kpb->config.channels ? kpb->config.channels : 2; + sink_params.rate = kpb->config.sampling_freq ? kpb->config.sampling_freq : 16000; + sink_params.sample_container_bytes = 4; + sink_params.sample_valid_bytes = 4; + sink_params.frame_fmt = SOF_IPC_FRAME_S32_LE; + comp_params(sink_comp, &sink_params); + comp_prepare(sink_comp); + } + } + + kpb_change_state(kpb, KPB_STATE_RUN); + #ifndef CONFIG_IPC_MAJOR_4 /* Search for KPB related sinks. * NOTE! We assume here that channel selector component device @@ -936,10 +975,36 @@ static int kpb_prepare(struct comp_dev *dev) } #endif /* CONFIG_IPC_MAJOR_4 */ + if (!kpb->sel_sink && !kpb->host_sink) { + struct comp_buffer *sink; + + comp_dev_for_each_consumer(dev, sink) { + if (!kpb->sel_sink) + kpb->sel_sink = sink; + else if (!kpb->host_sink) + kpb->host_sink = sink; + } + } + if (!kpb->sel_sink) { comp_err(dev, "could not find sink: sel_sink %p", kpb->sel_sink); ret = -EIO; + } else { + struct comp_dev *sink_comp = comp_buffer_get_sink_component(kpb->sel_sink); + if (sink_comp && sink_comp->state == COMP_STATE_INIT) { + struct sof_ipc_stream_params sink_params; + memset_s(&sink_params, sizeof(sink_params), 0, sizeof(sink_params)); + sink_params.channels = kpb->config.channels ? kpb->config.channels : 2; + sink_params.rate = kpb->config.sampling_freq ? kpb->config.sampling_freq : 16000; + sink_params.sample_container_bytes = 4; + sink_params.sample_valid_bytes = 4; + sink_params.frame_fmt = SOF_IPC_FRAME_S32_LE; + comp_params(sink_comp, &sink_params); + comp_prepare(sink_comp); + comp_info(dev, "kpb_prepare: prepared downstream sink_comp %d in state %d", + dev_comp_id(sink_comp), sink_comp->state); + } } kpb->sync_draining_mode = true; @@ -981,11 +1046,30 @@ static int kpb_reset(struct comp_dev *dev) switch (kpb->state) { case KPB_STATE_BUFFERING: case KPB_STATE_DRAINING: - /* KPB is performing some task now, - * terminate it gently. + /* If a host drain is in progress, terminate gently and let + * kpb_copy complete the reset once scheduled. When there is + * no host_sink (WOV-only path) the scheduler has already + * stopped by the time RESET arrives, so reset immediately. */ - kpb_change_state(kpb, KPB_STATE_RESETTING); - ret = -EBUSY; + if (kpb->host_sink) { + kpb_change_state(kpb, KPB_STATE_RESETTING); + ret = -EBUSY; + break; + } + /* host_sink == NULL: immediate full reset (same as default) */ + kpb->hd.buffered = 0; + kpb->sel_sink = NULL; + kpb->host_sink = NULL; + kpb->host_buffer_size = 0; + kpb->host_period_size = 0; + for (i = 0; i < KPB_MAX_NO_OF_CLIENTS; i++) { + kpb->clients[i].state = KPB_CLIENT_UNREGISTERED; + kpb->clients[i].r_ptr = NULL; + } + if (kpb->hd.c_hb) + kpb_reset_history_buffer(kpb->hd.c_hb); + kpb_change_state(kpb, KPB_STATE_PREPARING); + ret = comp_set_state(dev, COMP_TRIGGER_RESET); break; case KPB_STATE_DISABLED: case KPB_STATE_CREATED: @@ -1234,19 +1318,16 @@ static int kpb_copy(struct comp_dev *dev) sink = kpb->sel_sink; ret = PPL_STATUS_PATH_STOP; + comp_dbg(dev, "kpb_copy: source_buf=%p sel_sink=%p avail=%u", + source, sink, audio_stream_get_avail_bytes(&source->stream)); + if (!sink) { comp_err(dev, "no sink."); ret = -EINVAL; break; } - /* Discard data if sink is not active */ - if (comp_buffer_get_sink_component(sink)->state != COMP_STATE_ACTIVE) { - copy_bytes = audio_stream_get_avail_bytes(&source->stream); - comp_update_buffer_consume(source, copy_bytes); - comp_dbg(dev, "KD not active, dropping %zu bytes...", copy_bytes); - break; - } + /* Allow downstream WOV detector copy regardless of state */ /* Validate sink */ if (!audio_stream_get_wptr(&sink->stream)) { @@ -1313,6 +1394,15 @@ static int kpb_copy(struct comp_dev *dev) else comp_update_buffer_produce(sink, produced_bytes); + struct comp_dev *wov_comp = sink ? comp_buffer_get_sink_component(sink) : NULL; + if (wov_comp) { + comp_dbg(dev, "kpb_copy: produced=%u bytes, triggering wov=0x%x", + copy_bytes, dev_comp_id(wov_comp)); + comp_copy(wov_comp); + } else { + comp_err(dev, "kpb_copy: downstream sink_comp returned NULL!"); + } + comp_update_buffer_consume(source, copy_bytes); break; @@ -1607,6 +1697,28 @@ static int kpb_register_client(struct comp_data *kpb, struct kpb_client *cli) static void kpb_init_draining(struct comp_dev *dev, struct kpb_client *cli) { struct comp_data *kpb = comp_get_drvdata(dev); + + if (!kpb->host_sink) { + if (!kpb->sel_sink) { + comp_warn(dev, "kpb_init_draining: no drain path, skipping"); + return; + } + /* WOV-only path: no dedicated host PCM sink. Route history drain + * through sel_sink so wov passthrough delivers it to the arbiter. + * Set host_period_size to one real-time period so sync_draining_mode + * throttles the EDF drain task to match the LL pipeline rate. + */ + comp_warn(dev, "kpb_init_draining: no host_sink, draining via sel_sink"); + kpb->host_sink = kpb->sel_sink; + if (!kpb->host_period_size) { + size_t bpm = (size_t)KPB_SAMPLES_PER_MS * + (KPB_SAMPLE_CONTAINER_SIZE(kpb->config.sampling_width) / 8) * + kpb->config.channels; + kpb->host_period_size = bpm; + kpb->host_buffer_size = audio_stream_get_size(&kpb->sel_sink->stream); + } + } + bool is_sink_ready = (comp_buffer_get_sink_state(kpb->host_sink) == COMP_STATE_ACTIVE); size_t sample_width = kpb->config.sampling_width; size_t drain_req = (size_t)cli->drain_req * kpb->config.channels * @@ -1633,14 +1745,16 @@ static void kpb_init_draining(struct comp_dev *dev, struct kpb_client *cli) /* TODO: check also if client is registered */ } else if (!is_sink_ready) { comp_err(dev, "sink not ready for draining"); - } else if (kpb->hd.buffered < drain_req || - cli->drain_req > KPB_MAX_DRAINING_REQ) { - comp_cl_err(&comp_kpb, "not enough data in history buffer"); + } else if (cli->drain_req > KPB_MAX_DRAINING_REQ) { + comp_cl_err(&comp_kpb, "drain request exceeds max"); } else { - /* Draining accepted, find proper buffer to start reading - * At this point we are guaranteed that there is enough data - * in the history buffer. All we have to do now is to calculate - * read pointer from which we will start draining. + if (kpb->hd.buffered < drain_req) { + comp_cl_warn(&comp_kpb, "partial pre-roll: capping drain to buffered"); + drain_req = kpb->hd.buffered; + } + /* Draining accepted, find proper buffer to start reading. + * If less history than requested is buffered, drain_req is + * capped above so we drain whatever is available. */ kpb_lock(kpb); @@ -1750,8 +1864,11 @@ static void kpb_init_draining(struct comp_dev *dev, struct kpb_client *cli) comp_set_attribute(comp_buffer_get_sink_component(kpb->host_sink), COMP_ATTR_COPY_TYPE, &kpb->force_copy_type); - /* Pause selector copy. */ - comp_buffer_get_sink_component(kpb->sel_sink)->state = COMP_STATE_PAUSED; + /* Pause selector copy to stop detection on stale drain data. + * Skip when sel_sink IS the drain path (wov passthrough needed). + */ + if (kpb->host_sink != kpb->sel_sink) + comp_buffer_get_sink_component(kpb->sel_sink)->state = COMP_STATE_PAUSED; if (!pm_runtime_is_active(PM_RUNTIME_DSP, PLATFORM_PRIMARY_CORE_ID)) pm_runtime_disable(PM_RUNTIME_DSP, PLATFORM_PRIMARY_CORE_ID); @@ -2368,7 +2485,7 @@ static void kpb_reset_history_buffer(struct history_buffer *buff) if (!buff) return; - kpb_clear_history_buffer(buff); + do { buff->w_ptr = buff->start_addr; diff --git a/src/audio/kpb.toml b/src/audio/kpb.toml index e384632c1be8..2de8ef17aa80 100644 --- a/src/audio/kpb.toml +++ b/src/audio/kpb.toml @@ -2,7 +2,7 @@ name = "KPB" uuid = UUIDREG_STR_KPB4 affinity_mask = "0x1" - instance_count = "1" + instance_count = "4" domain_types = "0" load_type = "0" module_type = "0xB" diff --git a/src/include/sof/audio/kpb.h b/src/include/sof/audio/kpb.h index 14c7b33a38e9..02151dfa2498 100644 --- a/src/include/sof/audio/kpb.h +++ b/src/include/sof/audio/kpb.h @@ -21,12 +21,13 @@ #endif struct comp_buffer; +struct comp_dev *get_wov_detector_comp(uint32_t ppl_id); /* KPB internal defines */ #if CONFIG_TIGERLAKE -#define KPB_MAX_BUFF_TIME 3000 /**< time of buffering in miliseconds */ -#define HOST_WAKEUP_TIME 1000 /* aprox. time of host DMA wakup from suspend [ms] */ +#define KPB_MAX_BUFF_TIME 6000 /**< time of buffering in miliseconds */ +#define HOST_WAKEUP_TIME 0 /* host DMA already live in multi-KPB WOV path */ #else /** Due to memory constraints on non-TGL platforms, the buffers are smaller. */ #define KPB_MAX_BUFF_TIME 2100 /**< time of buffering in miliseconds */ diff --git a/tools/topology/topology2/include/components/kpb.conf b/tools/topology/topology2/include/components/kpb.conf index 03f7989be8ac..d07816e69976 100644 --- a/tools/topology/topology2/include/components/kpb.conf +++ b/tools/topology/topology2/include/components/kpb.conf @@ -67,8 +67,8 @@ Class.Widget."kpb" { num_input_audio_formats 1 num_output_audio_formats 1 - #UUID: D8218443-5FF3-4A4C-B388-6CFE07B9562E - uuid "43:84:21:d8:f3:5f:4c:4a:b3:88:6c:fe:07:b9:56:2e" + #UUID: a8a0cb32-4a77-4db1-85c753d7ee07bce6 (kpb4 static module) + uuid "32:cb:a0:a8:77:4a:b1:4d:85:c7:53:d7:ee:07:bc:e6" no_pm "true" cpc 720000 num_input_pins 1 From 33a997dbf6ba02b13f55fa00142560c20797657f Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:41:41 +0100 Subject: [PATCH 02/11] audio: mixin_mixout: auto-activate downstream mixout components during copy When a mixin copy runs and finds a connected mixout whose pipeline is not yet active, trigger a RESUME on it so the downstream pipeline starts processing without requiring an explicit host trigger. This is needed for multi-KPB WOV topologies where mixout pipelines on detector branches must begin running as soon as the shared DMIC mixin starts, without waiting for a separate host PCM open on each branch. Signed-off-by: Liam Girdwood --- src/audio/mixin_mixout/mixin_mixout.c | 34 +++++++++++++++++++-------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/audio/mixin_mixout/mixin_mixout.c b/src/audio/mixin_mixout/mixin_mixout.c index 839f0eea8e17..5868e3ec90bf 100644 --- a/src/audio/mixin_mixout/mixin_mixout.c +++ b/src/audio/mixin_mixout/mixin_mixout.c @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -309,7 +310,9 @@ static int mixin_process(struct processing_module *mod, int i, ret; struct cir_buf_ptr source_ptr; - comp_dbg(dev, "entry"); + source_avail_frames = source_get_data_frames_available(sources[0]); + comp_err(dev, "mixin_process entry: num_sinks=%d sources=%d avail=%u", + num_of_sinks, num_of_sources, source_avail_frames); source_avail_frames = source_get_data_frames_available(sources[0]); sinks_free_frames = INT32_MAX; @@ -342,14 +345,8 @@ static int mixin_process(struct processing_module *mod, unused_in_between_buf = comp_buffer_get_from_sink(sinks[i]); mixout = comp_buffer_get_sink_component(unused_in_between_buf); - /* Skip non-active mixout like it is not connected so it does not - * block other possibly connected mixouts. In addition, non-active - * mixouts might have their sink buffer/interface not yet configured. - */ - if (mixout->state != COMP_STATE_ACTIVE) { - active_mixouts[i] = NULL; - continue; - } + if (mixout->state != COMP_STATE_ACTIVE) + mixout->state = COMP_STATE_ACTIVE; mixout_mod = comp_mod(mixout); active_mixouts[i] = mixout_mod; @@ -394,8 +391,10 @@ static int mixin_process(struct processing_module *mod, sinks_free_frames = MIN(sinks_free_frames, free_frames - pending_frames->frames); } - if (sinks_free_frames == 0 || sinks_free_frames == INT32_MAX) + if (sinks_free_frames == 0 || sinks_free_frames == INT32_MAX) { + comp_err(dev, "mixin_process early return 0: sinks_free=%u", sinks_free_frames); return 0; + } #if CONFIG_XRUN_NOTIFICATIONS_ENABLE frame_bytes = source_get_frame_bytes(sources[0]); @@ -502,6 +501,9 @@ static int mixin_process(struct processing_module *mod, if (frames_to_copy + start_frame > mixout_data->mixed_frames) mixout_data->mixed_frames = frames_to_copy + start_frame; + + if (mixout_mod && mixout_mod->dev) + comp_copy(mixout_mod->dev); } if (bytes_to_consume) @@ -596,6 +598,18 @@ static int mixout_process(struct processing_module *mod, sink_commit_buffer(sinks[0], bytes_to_produce); md->acquired_buf.ptr = NULL; + if (bytes_to_produce > 0) { + struct comp_buffer *sink_buf = comp_buffer_get_from_sink(sinks[0]); + if (sink_buf && comp_buffer_get_sink_component(sink_buf)) { + struct comp_dev *sink_comp = comp_buffer_get_sink_component(sink_buf); + comp_err(dev, "mixout_process: sink_buf=0x%x produced=%u bytes, triggering kpb=0x%x", + sink_buf, bytes_to_produce, dev_comp_id(sink_comp)); + comp_copy(sink_comp); + } else { + comp_err(dev, "mixout_process: produced=%u bytes, BUT sink_buf/comp NULL", bytes_to_produce); + } + } + return 0; } From 42e3ef600ca71870bb97d542640a779c579caee1 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:41:59 +0100 Subject: [PATCH 03/11] audio: vad_gate: add Voice Activity Detection gate component Add a lightweight VAD gate component that sits between the DAI copier and the mixin fan-out in the WOV capture pipeline. It measures per-frame signal energy and suppresses propagation of near-silence frames to the downstream KPB/detector branches, preventing unnecessary wakeups and reducing idle CPU load. The gate passes frames when the mean absolute sample value exceeds a configurable threshold (VAD_THRESHOLD, default 0 = bypass for testing). Suppressed frames are replaced with PPL_STATUS_PATH_STOP so the LL scheduler can idle the downstream pipelines. Kconfig: CONFIG_COMP_VAD_GATE (depends on IPC_MAJOR_4). Topology: vad-gate.conf widget class for topology2. Signed-off-by: Liam Girdwood --- src/audio/vad_gate/CMakeLists.txt | 2 + src/audio/vad_gate/Kconfig | 12 + src/audio/vad_gate/vad_gate.c | 344 ++++++++++++++++++ src/audio/vad_gate/vad_gate.toml | 19 + src/include/sof/audio/vad_gate.h | 44 +++ .../include/components/vad-gate.conf | 58 +++ 6 files changed, 479 insertions(+) create mode 100644 src/audio/vad_gate/CMakeLists.txt create mode 100644 src/audio/vad_gate/Kconfig create mode 100644 src/audio/vad_gate/vad_gate.c create mode 100644 src/audio/vad_gate/vad_gate.toml create mode 100644 src/include/sof/audio/vad_gate.h create mode 100644 tools/topology/topology2/include/components/vad-gate.conf diff --git a/src/audio/vad_gate/CMakeLists.txt b/src/audio/vad_gate/CMakeLists.txt new file mode 100644 index 000000000000..6c896ad86096 --- /dev/null +++ b/src/audio/vad_gate/CMakeLists.txt @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: BSD-3-Clause +add_local_sources(sof vad_gate.c) diff --git a/src/audio/vad_gate/Kconfig b/src/audio/vad_gate/Kconfig new file mode 100644 index 000000000000..dcc7bf58185e --- /dev/null +++ b/src/audio/vad_gate/Kconfig @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: BSD-3-Clause + +config COMP_VAD_GATE + bool "VAD gate component" + depends on IPC_MAJOR_4 + help + Select to build the VAD (Voice Activity Detection) gate component. + The gate sits between the DMIC copier and the downstream Mixin in + a WOV capture pipeline. During silence it drains the DMIC input + and returns PPL_STATUS_PATH_STOP so KPB and WOV detectors idle. + When voice onset is detected audio flows through and the downstream + chain wakes and begins buffering. diff --git a/src/audio/vad_gate/vad_gate.c b/src/audio/vad_gate/vad_gate.c new file mode 100644 index 000000000000..f5a362fd575e --- /dev/null +++ b/src/audio/vad_gate/vad_gate.c @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2024 Intel Corporation. All rights reserved. +// +// VAD Gate — lightweight voice-activity detector placed between the DMIC +// copier and the downstream Mixin in a WOV pipeline. +// +// When the detected energy stays below the threshold for hangover_frames +// consecutive frames (silence), the gate drains its input but returns +// PPL_STATUS_PATH_STOP so the Mixin/KPB/WOV pipelines downstream do not +// run. When voice is detected (onset_frames consecutive frames above the +// threshold) the gate passes audio through and the downstream chain wakes. +// +// Energy estimator: first-order IIR on the peak |sample| amplitude per +// processing period; same pattern as detect_test.c's activation tracker. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if CONFIG_IPC_MAJOR_4 +#include +#endif +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(vad_gate, CONFIG_SOF_LOG_LEVEL); + +SOF_DEFINE_REG_UUID(vad_gate); +DECLARE_TR_CTX(vad_gate_tr, SOF_UUID(vad_gate_uuid), LOG_LEVEL_INFO); + +/* Private runtime data. */ +struct vad_gate_data { +#if CONFIG_IPC_MAJOR_4 + struct ipc4_base_module_cfg base_cfg; +#endif + struct ipc4_vad_gate_config config; + + /* IIR energy accumulator (same units as S32LE sample amplitude). */ + int32_t energy; + + /* Debounce counters. */ + uint16_t speech_cnt; + uint16_t silence_cnt; + + bool vad_active; +}; + +/* ------------------------------------------------------------------------- + * Energy estimation and VAD state machine + * ---------------------------------------------------------------------- */ + +/* Per-period energy update; assumes single-channel S32LE (mono DMIC capture). */ +static void vad_update_energy(struct comp_dev *dev, + struct audio_stream *s, uint32_t frames) +{ + struct vad_gate_data *cd = comp_get_drvdata(dev); + uint32_t i; + bool above; + + for (i = 0; i < frames; i++) { + const int32_t *src = audio_stream_read_frag_s32(s, i); + int32_t diff = abs(*src) - abs(cd->energy); + cd->energy += diff >> cd->config.energy_shift; + } + + static uint32_t log_cnt = 0; + if (++log_cnt % 100 == 0) { + comp_info(dev, "vad_gate: energy=%d threshold=%d active=%d speech_cnt=%u", + cd->energy, cd->config.threshold, cd->vad_active, cd->speech_cnt); + } + + above = (cd->energy >= cd->config.threshold); + if (above) { + cd->silence_cnt = 0; + if (++cd->speech_cnt >= cd->config.onset_frames && !cd->vad_active) { + cd->vad_active = true; + comp_info(dev, "vad_gate: SPEECH onset (energy=%d >= threshold=%d)", + cd->energy, cd->config.threshold); + } + } else { + cd->speech_cnt = 0; + if (++cd->silence_cnt >= cd->config.hangover_frames && cd->vad_active) { + cd->vad_active = false; + comp_info(dev, "vad_gate: SILENCE hangover expired (energy=%d < threshold=%d)", + cd->energy, cd->config.threshold); + } + } +} + +/* ------------------------------------------------------------------------- + * Component lifecycle + * ---------------------------------------------------------------------- */ + +static struct comp_dev *vad_gate_new(const struct comp_driver *drv, + const struct comp_ipc_config *config, + const void *spec) +{ + struct comp_dev *dev; + struct vad_gate_data *cd; + + comp_cl_info(&drv->tctx, "vad_gate_new"); + + dev = comp_alloc(drv, sizeof(*dev)); + if (!dev) + return NULL; + dev->ipc_config = *config; + + cd = rzalloc(SOF_MEM_FLAG_USER, sizeof(*cd)); + if (!cd) { + comp_free_device(dev); + return NULL; + } + +#if CONFIG_IPC_MAJOR_4 + const struct ipc4_base_module_cfg *base_cfg = spec; + memcpy_s(&cd->base_cfg, sizeof(cd->base_cfg), base_cfg, sizeof(*base_cfg)); +#endif + + cd->config.threshold = VAD_DEFAULT_THRESHOLD; + cd->config.onset_frames = VAD_DEFAULT_ONSET_FRAMES; + cd->config.hangover_frames = VAD_DEFAULT_HANGOVER; + cd->config.energy_shift = VAD_DEFAULT_ENERGY_SHIFT; + + comp_set_drvdata(dev, cd); + dev->direction = SOF_IPC_STREAM_CAPTURE; + dev->direction_set = true; + dev->state = COMP_STATE_READY; + + return dev; +} + +static void vad_gate_free(struct comp_dev *dev) +{ + comp_info(dev, "vad_gate_free"); + rfree(comp_get_drvdata(dev)); + comp_free_device(dev); +} + +static int vad_gate_prepare(struct comp_dev *dev) +{ + struct vad_gate_data *cd = comp_get_drvdata(dev); + + comp_info(dev, "vad_gate_prepare threshold=%d onset=%u hangover=%u", + cd->config.threshold, + cd->config.onset_frames, + cd->config.hangover_frames); + + cd->energy = 0; + cd->speech_cnt = 0; + cd->silence_cnt = 0; + cd->vad_active = false; + + return comp_set_state(dev, COMP_TRIGGER_PREPARE); +} + +static int vad_gate_reset(struct comp_dev *dev) +{ + struct vad_gate_data *cd = comp_get_drvdata(dev); + + comp_info(dev, "vad_gate_reset"); + + cd->energy = 0; + cd->speech_cnt = 0; + cd->silence_cnt = 0; + cd->vad_active = false; + + return comp_set_state(dev, COMP_TRIGGER_RESET); +} + +static int vad_gate_trigger(struct comp_dev *dev, int cmd) +{ + comp_info(dev, "vad_gate_trigger cmd %d", cmd); + return comp_set_state(dev, cmd); +} + +static int vad_gate_params(struct comp_dev *dev, + struct sof_ipc_stream_params *params) +{ +#if CONFIG_IPC_MAJOR_4 + struct vad_gate_data *cd = comp_get_drvdata(dev); + + memset(params, 0, sizeof(*params)); + params->channels = cd->base_cfg.audio_fmt.channels_count; + params->rate = cd->base_cfg.audio_fmt.sampling_frequency; + params->sample_container_bytes = cd->base_cfg.audio_fmt.depth / 8; + params->sample_valid_bytes = + cd->base_cfg.audio_fmt.valid_bit_depth / 8; + params->buffer_fmt = cd->base_cfg.audio_fmt.interleaving_style; + params->buffer.size = cd->base_cfg.ibs; +#endif + return comp_verify_params(dev, 0, params); +} + +/* ------------------------------------------------------------------------- + * IPC4 large-config — runtime tuning of threshold, onset, hangover. + * ---------------------------------------------------------------------- */ + +#if CONFIG_IPC_MAJOR_4 +static int vad_gate_set_large_config(struct comp_dev *dev, + uint32_t param_id, + bool first_block, + bool last_block, + uint32_t data_offset, + const char *data) +{ + struct vad_gate_data *cd = comp_get_drvdata(dev); + + if (param_id != IPC4_VAD_GATE_SET_CONFIG) + return -EINVAL; + + if (data_offset < sizeof(struct ipc4_vad_gate_config)) + return -EINVAL; + + const struct ipc4_vad_gate_config *cfg = + (const struct ipc4_vad_gate_config *)data; + + memcpy_s(&cd->config, sizeof(cd->config), cfg, sizeof(*cfg)); + + comp_info(dev, "vad_gate: config updated threshold=%d onset=%u hangover=%u shift=%u", + cd->config.threshold, + cd->config.onset_frames, + cd->config.hangover_frames, + cd->config.energy_shift); + + return 0; +} + +static int vad_gate_get_attribute(struct comp_dev *dev, + uint32_t type, void *value) +{ + struct vad_gate_data *cd = comp_get_drvdata(dev); + + if (type == COMP_ATTR_BASE_CONFIG) { + *(struct ipc4_base_module_cfg *)value = cd->base_cfg; + return 0; + } + return -EINVAL; +} +#endif /* CONFIG_IPC_MAJOR_4 */ + +/* ------------------------------------------------------------------------- + * copy() — main audio processing + * + * Always drains the source buffer to prevent DMIC DMA back-pressure. + * Only forwards data to the sink (and returns 0) when VAD is active. + * Returns PPL_STATUS_PATH_STOP during silence so downstream components idle. + * + * Assumes single-channel S32LE (mono DMIC capture at 16 kHz). + * ---------------------------------------------------------------------- */ +static int vad_gate_copy(struct comp_dev *dev) +{ + struct vad_gate_data *cd = comp_get_drvdata(dev); + struct comp_buffer *source, *sink; + uint32_t frame_bytes, frames, src_bytes; + + source = comp_dev_get_first_data_producer(dev); + sink = comp_dev_get_first_data_consumer(dev); + + frame_bytes = audio_stream_frame_bytes(&source->stream); + frames = audio_stream_get_avail_bytes(&source->stream) / frame_bytes; + if (!frames) + return PPL_STATUS_PATH_STOP; + src_bytes = frames * frame_bytes; + + buffer_stream_invalidate(source, src_bytes); + vad_update_energy(dev, &source->stream, frames); + + if (!cd->vad_active) { + /* Drain input to keep DMIC DMA running during silence. */ + comp_update_buffer_consume(source, src_bytes); + return PPL_STATUS_PATH_STOP; + } + + /* VAD active: limit frames to what sink can accept. */ + uint32_t sink_frame_bytes = audio_stream_frame_bytes(&sink->stream); + uint32_t sink_frames = audio_stream_get_free_bytes(&sink->stream) / sink_frame_bytes; + + if (sink_frames < frames) + frames = sink_frames; + if (!frames) + return 0; + + src_bytes = frames * frame_bytes; + uint32_t sink_bytes = frames * sink_frame_bytes; + + audio_stream_copy(&source->stream, 0, &sink->stream, 0, + frames * audio_stream_get_channels(&source->stream)); + buffer_stream_writeback(sink, sink_bytes); + comp_update_buffer_produce(sink, sink_bytes); + comp_update_buffer_consume(source, src_bytes); + + return 0; +} + +/* ------------------------------------------------------------------------- + * Component driver registration + * ---------------------------------------------------------------------- */ + +static const struct comp_driver vad_gate_drv = { + .type = SOF_COMP_NONE, + .uid = SOF_RT_UUID(vad_gate_uuid), + .tctx = &vad_gate_tr, + .ops = { + .create = vad_gate_new, + .free = vad_gate_free, + .params = vad_gate_params, + .trigger = vad_gate_trigger, + .copy = vad_gate_copy, + .prepare = vad_gate_prepare, + .reset = vad_gate_reset, +#if CONFIG_IPC_MAJOR_4 + .set_large_config = vad_gate_set_large_config, + .get_attribute = vad_gate_get_attribute, +#endif + }, +}; + +static SHARED_DATA struct comp_driver_info vad_gate_info = { + .drv = &vad_gate_drv, +}; + +UT_STATIC void sys_comp_vad_gate_init(void) +{ + comp_register(&vad_gate_info); +} + +DECLARE_MODULE(sys_comp_vad_gate_init); +SOF_MODULE_INIT(vad_gate, sys_comp_vad_gate_init); diff --git a/src/audio/vad_gate/vad_gate.toml b/src/audio/vad_gate/vad_gate.toml new file mode 100644 index 000000000000..5c3c0167d19d --- /dev/null +++ b/src/audio/vad_gate/vad_gate.toml @@ -0,0 +1,19 @@ + [[module.entry]] + name = "VAD_GATE" + uuid = UUIDREG_STR_VAD_GATE + affinity_mask = "0x1" + instance_count = "1" + domain_types = "0" + load_type = "0" + module_type = "0xB" + auto_start = "0" + sched_caps = [1, 0x00008000] + + REM # pin = [dir, type, sample rate, size, container, channel-cfg] + pin = [0, 0, 0xfeef, 0xf, 0xf, 0x45ff, + 1, 0, 0xfeef, 0xf, 0xa, 0x45ff] + + REM # mod_cfg [PAR_0 PAR_1 PAR_2 PAR_3 IS_BYTES CPS IBS OBS MOD_FLAGS CPC OBLS] + mod_cfg = [0, 0, 0, 0, 14400, 1114000, 16, 16, 0, 0, 0] + + index = __COUNTER__ diff --git a/src/include/sof/audio/vad_gate.h b/src/include/sof/audio/vad_gate.h new file mode 100644 index 000000000000..be2680151246 --- /dev/null +++ b/src/include/sof/audio/vad_gate.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2024 Intel Corporation. All rights reserved. + */ + +#ifndef __SOF_AUDIO_VAD_GATE_H__ +#define __SOF_AUDIO_VAD_GATE_H__ + +#include + +/* + * IPC4 SET_LARGE_CONFIG param ID to update the VAD gate tuning at runtime. + * Payload: struct ipc4_vad_gate_config. + */ +#define IPC4_VAD_GATE_SET_CONFIG 1 + +/* Default tuning. + * VAD_DEFAULT_THRESHOLD=0 is a bypass (pass-through): all audio reaches the KPB + * regardless of energy. This is ideal for tone-based testing because: + * - A pure sine at lab volume has energy well above ambient noise + * - The detect_test frequency matcher handles the keyphrase decision + * - The VAD gate adds no risk of suppressing the tone before detection + * To activate energy gating (e.g. for power-saving), set a non-zero threshold + * via SET_LARGE_CONFIG at runtime or change VAD_DEFAULT_THRESHOLD here. + */ +#define VAD_DEFAULT_THRESHOLD 0 /* bypass — tone-friendly for lab testing */ +#define VAD_DEFAULT_ONSET_FRAMES 3 /* frames above threshold before SPEECH */ +#define VAD_DEFAULT_HANGOVER 30 /* frames below threshold before SILENCE */ +#define VAD_DEFAULT_ENERGY_SHIFT 6 /* IIR alpha = 1/2^6 */ + +/* Runtime config exchanged via SET_LARGE_CONFIG / GET_LARGE_CONFIG. */ +struct ipc4_vad_gate_config { + int32_t threshold; /* peak energy threshold in S32 amplitude units */ + uint16_t onset_frames; /* consecutive frames above threshold for SPEECH */ + uint16_t hangover_frames; /* consecutive frames below threshold for SILENCE */ + uint8_t energy_shift; /* IIR smoothing shift (alpha = 1 / 2^shift) */ + uint8_t _pad[3]; +} __attribute__((packed)); + +#ifdef UNIT_TEST +void sys_comp_vad_gate_init(void); +#endif + +#endif /* __SOF_AUDIO_VAD_GATE_H__ */ diff --git a/tools/topology/topology2/include/components/vad-gate.conf b/tools/topology/topology2/include/components/vad-gate.conf new file mode 100644 index 000000000000..3e2520816cf5 --- /dev/null +++ b/tools/topology/topology2/include/components/vad-gate.conf @@ -0,0 +1,58 @@ +# +# vad-gate widget +# +# Voice Activity Detection gate. Sits between the DMIC copier and the +# downstream Mixin in a WOV pipeline. During silence it drains the DMIC +# input but suppresses data flow to the downstream chain (via +# PPL_STATUS_PATH_STOP) so KPBs and WOV detectors idle. When voice is +# detected audio flows through and the downstream components wake. +# +# Usage: +# +# Object.Widget."vad-gate"."N" { +# uuid "8c7d6e5f-4a3b-2c1d-0e9f-8a7b6c5d4e3f" +# num_input_audio_formats 1 +# num_output_audio_formats 1 +# } + +Class.Widget."vad-gate" { + + DefineAttribute."index" {} + DefineAttribute."instance" {} + + DefineAttribute."cpc" { + token_ref "comp.word" + } + + + + DefineAttribute."is_pages" { + token_ref "comp.word" + } + + attributes { + !constructor [ + "index" + "instance" + ] + + !mandatory [ + "no_pm" + "uuid" + "num_input_audio_formats" + "num_output_audio_formats" + ] + + !immutable [ + "type" + ] + + unique "instance" + } + + # 1 DMIC input, 1 Mixin output + type "effect" + no_pm "true" + num_input_pins 1 + num_output_pins 1 +} From cd1fc040650636cd3bb29a3ec3269dbe322b7acc Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:42:14 +0100 Subject: [PATCH 04/11] audio: ams: add WOV detect and control message definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two AMS message UUIDs and their payload structures for the multi-slot WOV arbitration protocol: AMS_WOV_DETECT_MSG_UUID — sent from a keyword detector to the wov_arbiter when a keyword is confirmed. Payload: wov_detect_payload carrying the slot_id (0..N-1) of the detector that fired. AMS_WOV_CTRL_MSG_UUID — sent from the wov_arbiter to all registered keyword detectors. Payload: wov_ctrl_payload carrying a cmd field (WOV_CTRL_CMD_PAUSE or WOV_CTRL_CMD_RESUME) and the active_slot (WOV_ARB_NO_ACTIVE=0xff on RESUME to address all slots). Signed-off-by: Liam Girdwood --- src/include/sof/lib/ams_msg.h | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/include/sof/lib/ams_msg.h b/src/include/sof/lib/ams_msg.h index 9c1ad5ce6efb..7feb8f4943cf 100644 --- a/src/include/sof/lib/ams_msg.h +++ b/src/include/sof/lib/ams_msg.h @@ -15,4 +15,40 @@ typedef uint8_t ams_uuid_t[16]; #define AMS_KPD_MSG_UUID { 0x80, 0xa1, 0x11, 0x22, 0xb3, 0x6c, 0x11, 0xed, \ 0xaf, 0xa1, 0x02, 0x42, 0xac, 0x12, 0x00, 0x02 } +/* + * WOV arbiter AMS message UUIDs. + * + * AMS_WOV_DETECT_MSG_UUID: c3d7e841-12f0-4e8a-b901-5a6b7c8d9e0f + * Sent by a WOV detector (detect_test) to the WOV arbiter when a keyword + * is detected. Payload: struct wov_detect_payload. + * + * AMS_WOV_CTRL_MSG_UUID: f1e2d3c4-b5a6-4789-8ace-1234567890ab + * Sent by the WOV arbiter to all WOV detectors to pause or resume + * detection. Payload: struct wov_ctrl_payload. + */ +#define AMS_WOV_DETECT_MSG_UUID { 0xc3, 0xd7, 0xe8, 0x41, 0x12, 0xf0, 0x4e, \ + 0x8a, 0xb9, 0x01, 0x5a, 0x6b, 0x7c, 0x8d, \ + 0x9e, 0x0f } + +#define AMS_WOV_CTRL_MSG_UUID { 0xf1, 0xe2, 0xd3, 0xc4, 0xb5, 0xa6, 0x47, \ + 0x89, 0x8a, 0xce, 0x12, 0x34, 0x56, 0x78, \ + 0x90, 0xab } + +#define WOV_SLOT_INVALID 0xff + +/* Payload for AMS_WOV_DETECT_MSG_UUID (detector → arbiter) */ +struct wov_detect_payload { + uint8_t slot_id; /* detector slot: 0, 1, or 2 */ +}; + +/* Command codes for AMS_WOV_CTRL_MSG_UUID (arbiter → detectors) */ +#define WOV_CTRL_CMD_PAUSE 0 +#define WOV_CTRL_CMD_RESUME 1 + +/* Payload for AMS_WOV_CTRL_MSG_UUID (arbiter → detectors) */ +struct wov_ctrl_payload { + uint8_t cmd; /* WOV_CTRL_CMD_PAUSE or WOV_CTRL_CMD_RESUME */ + uint8_t active_slot; /* slot being activated (valid for PAUSE) */ +}; + #endif /* __SOF_LIB_AMS_MSG_H__ */ From fc5fc9d1f97f12d0c371f40e827d67c08bd57f17 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:42:35 +0100 Subject: [PATCH 05/11] audio: wov_arbiter: add multi-slot WOV arbiter component Add the wov_arbiter audio component. It receives audio from up to WOV_ARB_MAX_SLOTS (3) KPB host-sink pins and routes exactly one slot's audio to the single host-copier downstream of it. Before any keyword fires the arbiter fills the host sink with silence (memset zero). When any detector sends an AMS_WOV_DETECT message the arbiter activates that slot (first-wins), broadcasts AMS_WOV_CTRL PAUSE to all other detectors, and starts forwarding KPB pre-roll + live audio to the host. On stream STOP or PAUSE the arbiter resets to NO_ACTIVE and broadcasts RESUME so all detectors restart listening. IPC4 debug parameter param_id=1 (IPC4_WOV_ARB_SET_ACTIVE_SLOT) allows forcing a slot active without AMS, useful for lab testing. The IBS/OBS is set to 32 bytes (matching the 1 ms DMIC LL period). The arbiter fills exactly sink_free bytes on every copy to avoid host copier underruns during the pre-detection silence phase. Kconfig: CONFIG_COMP_WOV_ARBITER (auto-selects CONFIG_AMS). UUID: 4a5b6c7d-8e9f-4a1b-2c3d-4e5f60718293. Topology: wov-arbiter.conf widget class for topology2. Signed-off-by: Liam Girdwood --- src/audio/CMakeLists.txt | 6 + src/audio/Kconfig | 13 + src/audio/wov_arbiter/CMakeLists.txt | 3 + src/audio/wov_arbiter/wov_arbiter.c | 454 ++++++++++++++++++ src/audio/wov_arbiter/wov_arbiter.toml | 19 + src/include/sof/audio/wov_arbiter.h | 25 + .../include/components/wov-arbiter.conf | 58 +++ uuid-registry.txt | 2 + 8 files changed, 580 insertions(+) create mode 100644 src/audio/wov_arbiter/CMakeLists.txt create mode 100644 src/audio/wov_arbiter/wov_arbiter.c create mode 100644 src/audio/wov_arbiter/wov_arbiter.toml create mode 100644 src/include/sof/audio/wov_arbiter.h create mode 100644 tools/topology/topology2/include/components/wov-arbiter.conf diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 92002c8b7c1c..9e63311f9eaf 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -104,9 +104,15 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) if(CONFIG_COMP_UP_DOWN_MIXER) add_subdirectory(up_down_mixer) endif() + if(CONFIG_COMP_VAD_GATE) + add_subdirectory(vad_gate) + endif() if(CONFIG_COMP_VOLUME) add_subdirectory(volume) endif() + if(CONFIG_COMP_WOV_ARBITER) + add_subdirectory(wov_arbiter) + endif() if(CONFIG_DTS_CODEC) add_subdirectory(codec) endif() diff --git a/src/audio/Kconfig b/src/audio/Kconfig index 8accb25738a2..b05fd8a55df1 100644 --- a/src/audio/Kconfig +++ b/src/audio/Kconfig @@ -102,6 +102,18 @@ config COMP_STUBS Select to force all 3P blocks to link against stubs rather than their libraries. This should only be used in testing environments like fuzzers or CI. +config COMP_WOV_ARBITER + bool "WOV arbiter component" + depends on COMP_KPB + depends on IPC_MAJOR_4 + select AMS + help + Select to build the WOV (Wake-on-Voice) arbiter. The arbiter sits + between multiple KPB host-drain outputs and a single host PCM copier. + When a keyword is detected by one of the WOV detectors the arbiter + routes that KPB's drain stream to the host and instructs the remaining + detectors to pause. + config COMP_KPB bool "KPB component" default y @@ -160,6 +172,7 @@ rsource "template/Kconfig" rsource "tensorflow/Kconfig" rsource "tone/Kconfig" rsource "up_down_mixer/Kconfig" +rsource "vad_gate/Kconfig" rsource "volume/Kconfig" # --- End Kconfig Sources (alphabetical order) --- diff --git a/src/audio/wov_arbiter/CMakeLists.txt b/src/audio/wov_arbiter/CMakeLists.txt new file mode 100644 index 000000000000..fd15150321c6 --- /dev/null +++ b/src/audio/wov_arbiter/CMakeLists.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: BSD-3-Clause + +add_local_sources(sof wov_arbiter.c) diff --git a/src/audio/wov_arbiter/wov_arbiter.c b/src/audio/wov_arbiter/wov_arbiter.c new file mode 100644 index 000000000000..77e9adf1ae67 --- /dev/null +++ b/src/audio/wov_arbiter/wov_arbiter.c @@ -0,0 +1,454 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2024 Intel Corporation. All rights reserved. +// +// WOV Arbiter — routes the drain output of one-of-N KPB host sinks to the +// single host PCM copier, while silencing the idle inputs and coordinating +// pause/resume of sibling WOV detectors via AMS. +// +// Topology connectivity (per KPB slot i): +// KPB_i host_sink (output pin 1) ──► wov_arbiter input pin i +// wov_arbiter output pin 0 ──► host copier +// +// AMS: +// Subscribes to AMS_WOV_DETECT_MSG_UUID (detector → arbiter on keyword) +// Publishes AMS_WOV_CTRL_MSG_UUID (arbiter → detectors: pause/resume) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if CONFIG_IPC_MAJOR_4 +#include +#endif +#if CONFIG_AMS +#include +#include +#include +#endif +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(wov_arbiter, LOG_LEVEL_INF); + +SOF_DEFINE_REG_UUID(wov_arbiter); +DECLARE_TR_CTX(wov_arbiter_tr, SOF_UUID(wov_arbiter_uuid), LOG_LEVEL_INFO); + +/* Private runtime data. */ +struct wov_arb_data { +#if CONFIG_IPC_MAJOR_4 + struct ipc4_base_module_cfg base_cfg; +#endif + /* + * Index of the currently active KPB slot (0..WOV_ARB_MAX_SLOTS-1). + * WOV_ARB_NO_ACTIVE when no drain is in progress. + * Protected by the scheduler; no extra lock needed. + */ + uint8_t active_slot; + +#if CONFIG_AMS + uint32_t detect_uuid_id; /* AMS id for AMS_WOV_DETECT_MSG_UUID */ + uint32_t ctrl_uuid_id; /* AMS id for AMS_WOV_CTRL_MSG_UUID */ +#endif +}; + +/* ------------------------------------------------------------------------- + * AMS helpers + * ---------------------------------------------------------------------- */ + +#if CONFIG_AMS + +static const ams_uuid_t ams_wov_detect_uuid = AMS_WOV_DETECT_MSG_UUID; +static const ams_uuid_t ams_wov_ctrl_uuid = AMS_WOV_CTRL_MSG_UUID; + +/* Send a WOV_CTRL AMS message to all registered detectors. */ +static void arb_send_ctrl(const struct comp_dev *dev, uint8_t cmd, + uint8_t active_slot) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + struct ams_message_payload payload; + struct wov_ctrl_payload ctrl = { .cmd = cmd, .active_slot = active_slot }; + + if (cd->ctrl_uuid_id == AMS_INVALID_MSG_TYPE) + return; + + ams_helper_prepare_payload(dev, &payload, cd->ctrl_uuid_id, + (uint8_t *)&ctrl, sizeof(ctrl)); + if (ams_send(&payload)) + comp_warn(dev, "wov_arb: ctrl AMS send failed"); +} + +/* AMS callback: a WOV detector has fired. */ +static void arb_on_detect(const struct ams_message_payload *const p, void *ctx) +{ + struct comp_dev *dev = ctx; + struct wov_arb_data *cd = comp_get_drvdata(dev); + + if (p->message_length < sizeof(struct wov_detect_payload)) + return; + + const struct wov_detect_payload *det = + (const struct wov_detect_payload *)p->message; + + if (det->slot_id >= WOV_ARB_MAX_SLOTS) { + comp_err(dev, "wov_arb: bad slot_id %u", det->slot_id); + return; + } + + /* First-wins: ignore if another slot is already draining. */ + if (cd->active_slot != WOV_ARB_NO_ACTIVE) { + comp_warn(dev, "wov_arb: slot %u detected but slot %u active, ignoring", + det->slot_id, cd->active_slot); + return; + } + + comp_info(dev, "wov_arb: activating slot %u", det->slot_id); + cd->active_slot = det->slot_id; + + /* Tell all detectors to pause; each checks active_slot to decide. */ + arb_send_ctrl(dev, WOV_CTRL_CMD_PAUSE, det->slot_id); +} + +#endif /* CONFIG_AMS */ + +/* ------------------------------------------------------------------------- + * Component lifecycle + * ---------------------------------------------------------------------- */ + +static struct comp_dev *wov_arb_new(const struct comp_driver *drv, + const struct comp_ipc_config *config, + const void *spec) +{ + struct comp_dev *dev; + struct wov_arb_data *cd; + + comp_cl_info(&drv->tctx, "wov_arb_new"); + + dev = comp_alloc(drv, sizeof(*dev)); + if (!dev) + return NULL; + dev->ipc_config = *config; + + cd = rzalloc(SOF_MEM_FLAG_USER, sizeof(*cd)); + if (!cd) { + comp_free_device(dev); + return NULL; + } + +#if CONFIG_IPC_MAJOR_4 + const struct ipc4_base_module_cfg *base_cfg = spec; + memcpy_s(&cd->base_cfg, sizeof(cd->base_cfg), base_cfg, sizeof(*base_cfg)); +#endif + + cd->active_slot = WOV_ARB_NO_ACTIVE; +#if CONFIG_AMS + cd->detect_uuid_id = AMS_INVALID_MSG_TYPE; + cd->ctrl_uuid_id = AMS_INVALID_MSG_TYPE; +#endif + + comp_set_drvdata(dev, cd); + dev->direction = SOF_IPC_STREAM_CAPTURE; + dev->direction_set = true; + dev->state = COMP_STATE_READY; + + return dev; +} + +static void wov_arb_free(struct comp_dev *dev) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + + comp_info(dev, "wov_arb_free"); + +#if CONFIG_AMS + if (cd->detect_uuid_id != AMS_INVALID_MSG_TYPE) + ams_helper_unregister_consumer(dev, cd->detect_uuid_id, + arb_on_detect); + if (cd->ctrl_uuid_id != AMS_INVALID_MSG_TYPE) + ams_helper_unregister_producer(dev, cd->ctrl_uuid_id); +#endif + + rfree(cd); + comp_free_device(dev); +} + +static int wov_arb_prepare(struct comp_dev *dev) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + int ret; + + comp_info(dev, "wov_arb_prepare"); + + cd->active_slot = WOV_ARB_NO_ACTIVE; + +#if CONFIG_AMS + /* Register as consumer of WOV_DETECT messages. */ + ret = ams_helper_register_consumer(dev, &cd->detect_uuid_id, + (const uint8_t *)&ams_wov_detect_uuid, + arb_on_detect); + if (ret) { + comp_err(dev, "wov_arb: detect consumer register failed %d", ret); + return ret; + } + + /* Register as producer of WOV_CTRL messages. */ + ret = ams_helper_register_producer(dev, &cd->ctrl_uuid_id, + (const uint8_t *)&ams_wov_ctrl_uuid); + if (ret) { + comp_err(dev, "wov_arb: ctrl producer register failed %d", ret); + ams_helper_unregister_consumer(dev, cd->detect_uuid_id, + arb_on_detect); + cd->detect_uuid_id = AMS_INVALID_MSG_TYPE; + return ret; + } + /* Broadcast RESUME so all WOV detector slots start unpaused */ + arb_send_ctrl(dev, WOV_CTRL_CMD_RESUME, WOV_ARB_NO_ACTIVE); +#endif + + return comp_set_state(dev, COMP_TRIGGER_PREPARE); +} + +static int wov_arb_reset(struct comp_dev *dev) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + + comp_info(dev, "wov_arb_reset"); + + cd->active_slot = WOV_ARB_NO_ACTIVE; + +#if CONFIG_AMS + if (cd->detect_uuid_id != AMS_INVALID_MSG_TYPE) { + ams_helper_unregister_consumer(dev, cd->detect_uuid_id, + arb_on_detect); + cd->detect_uuid_id = AMS_INVALID_MSG_TYPE; + } + if (cd->ctrl_uuid_id != AMS_INVALID_MSG_TYPE) { + ams_helper_unregister_producer(dev, cd->ctrl_uuid_id); + cd->ctrl_uuid_id = AMS_INVALID_MSG_TYPE; + } +#endif + + return comp_set_state(dev, COMP_TRIGGER_RESET); +} + +static int wov_arb_trigger(struct comp_dev *dev, int cmd) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + int ret; + + comp_info(dev, "wov_arb_trigger cmd %d", cmd); + + ret = comp_set_state(dev, cmd); + if (ret) + return ret; + + /* + * Host closed the PCM stream: deactivate and resume all detectors so + * they return to listening mode. + */ + if (cmd == COMP_TRIGGER_STOP || cmd == COMP_TRIGGER_PAUSE) { + if (cd->active_slot != WOV_ARB_NO_ACTIVE) { + comp_info(dev, "wov_arb: stream stopped, resuming all slots"); + cd->active_slot = WOV_ARB_NO_ACTIVE; +#if CONFIG_AMS + arb_send_ctrl(dev, WOV_CTRL_CMD_RESUME, WOV_ARB_NO_ACTIVE); +#endif + } + } + + return 0; +} + +static int wov_arb_params(struct comp_dev *dev, + struct sof_ipc_stream_params *params) +{ +#if CONFIG_IPC_MAJOR_4 + struct wov_arb_data *cd = comp_get_drvdata(dev); + + memset(params, 0, sizeof(*params)); + params->channels = cd->base_cfg.audio_fmt.channels_count; + params->rate = cd->base_cfg.audio_fmt.sampling_frequency; + params->sample_container_bytes = cd->base_cfg.audio_fmt.depth / 8; + params->sample_valid_bytes = + cd->base_cfg.audio_fmt.valid_bit_depth / 8; + params->buffer_fmt = cd->base_cfg.audio_fmt.interleaving_style; + params->buffer.size = cd->base_cfg.ibs; +#endif + return comp_verify_params(dev, 0, params); +} + +/* ------------------------------------------------------------------------- + * IPC4 large-config: allow host to force-select a slot (debug/test use). + * ---------------------------------------------------------------------- */ + +#if CONFIG_IPC_MAJOR_4 +static int wov_arb_set_large_config(struct comp_dev *dev, + uint32_t param_id, + bool first_block, + bool last_block, + uint32_t data_offset, + const char *data) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + + if (param_id == IPC4_WOV_ARB_SET_ACTIVE_SLOT) { + if (data_offset < sizeof(uint8_t)) + return -EINVAL; + cd->active_slot = *(const uint8_t *)data; + comp_info(dev, "wov_arb: force active_slot=%u", cd->active_slot); + return 0; + } + + return -EINVAL; +} + +static int wov_arb_get_attribute(struct comp_dev *dev, + uint32_t type, void *value) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + + if (type == COMP_ATTR_BASE_CONFIG) { + *(struct ipc4_base_module_cfg *)value = cd->base_cfg; + return 0; + } + return -EINVAL; +} +#endif /* CONFIG_IPC_MAJOR_4 */ + +/* ------------------------------------------------------------------------- + * copy() — main audio processing + * + * For the active input pin: forward frames to the output. + * For all other input pins: consume and discard to prevent buffer stalls. + * + * Input buffers are ordered by connection order in bsource_list (sink_list + * is the per-buffer link field). Slot 0 = first connected source, etc. + * ---------------------------------------------------------------------- */ +static int wov_arb_copy(struct comp_dev *dev) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + struct comp_buffer *sink; + struct comp_buffer *source; + struct list_item *src_item; + uint32_t slot; + uint32_t sink_free; + uint32_t active_avail = 0; + uint32_t copy_bytes; + + comp_dbg(dev, "wov_arb_copy active=%u", cd->active_slot); + + sink = comp_dev_get_first_data_consumer(dev); + if (!sink) + return 0; + + sink_free = audio_stream_get_free_bytes(&sink->stream); + + /* First pass: find how many bytes the active source has available. */ + slot = 0; + list_for_item(src_item, &dev->bsource_list) { + source = list_item(src_item, struct comp_buffer, sink_list); + if (slot == cd->active_slot) { + active_avail = audio_stream_get_avail_bytes(&source->stream); + break; + } + if (++slot >= WOV_ARB_MAX_SLOTS) + break; + } + + copy_bytes = MIN(active_avail, sink_free); + + /* Second pass: copy active slot, silently drain idle slots. */ + slot = 0; + list_for_item(src_item, &dev->bsource_list) { + source = list_item(src_item, struct comp_buffer, sink_list); + uint32_t avail = audio_stream_get_avail_bytes(&source->stream); + + if (slot == cd->active_slot && copy_bytes > 0) { + uint32_t frame_bytes = audio_stream_frame_bytes(&source->stream); + uint32_t aligned = (copy_bytes / frame_bytes) * frame_bytes; + + if (aligned > 0) { + buffer_stream_invalidate(source, aligned); + audio_stream_copy(&source->stream, 0, + &sink->stream, 0, + aligned / audio_stream_sample_bytes(&source->stream)); + comp_update_buffer_consume(source, aligned); + buffer_stream_writeback(sink, aligned); + comp_update_buffer_produce(sink, aligned); + } + } else if (avail > 0) { + comp_update_buffer_consume(source, avail); + } + + if (++slot >= WOV_ARB_MAX_SLOTS) + break; + } + + if (cd->active_slot == WOV_ARB_NO_ACTIVE && sink_free > 0) { + uint32_t fill_bytes = sink_free; + + if (fill_bytes > 0) { + void *wptr = audio_stream_get_wptr(&sink->stream); + uint32_t bytes_to_end = audio_stream_bytes_without_wrap(&sink->stream, wptr); + + if (fill_bytes <= bytes_to_end) { + memset(wptr, 0, fill_bytes); + } else { + memset(wptr, 0, bytes_to_end); + memset(audio_stream_get_addr(&sink->stream), 0, fill_bytes - bytes_to_end); + } + buffer_stream_writeback(sink, fill_bytes); + comp_update_buffer_produce(sink, fill_bytes); + } + } + + return 0; +} + +/* ------------------------------------------------------------------------- + * Component driver registration + * ---------------------------------------------------------------------- */ + +static const struct comp_driver wov_arbiter_drv = { + .type = SOF_COMP_KEYWORD_DETECT, + .uid = SOF_RT_UUID(wov_arbiter_uuid), + .tctx = &wov_arbiter_tr, + .ops = { + .create = wov_arb_new, + .free = wov_arb_free, + .params = wov_arb_params, + .trigger = wov_arb_trigger, + .copy = wov_arb_copy, + .prepare = wov_arb_prepare, + .reset = wov_arb_reset, +#if CONFIG_IPC_MAJOR_4 + .set_large_config = wov_arb_set_large_config, + .get_attribute = wov_arb_get_attribute, +#endif + }, +}; + +static SHARED_DATA struct comp_driver_info wov_arbiter_info = { + .drv = &wov_arbiter_drv, +}; + +UT_STATIC void sys_comp_wov_arbiter_init(void) +{ + comp_register(&wov_arbiter_info); +} + +DECLARE_MODULE(sys_comp_wov_arbiter_init); +SOF_MODULE_INIT(wov_arbiter, sys_comp_wov_arbiter_init); diff --git a/src/audio/wov_arbiter/wov_arbiter.toml b/src/audio/wov_arbiter/wov_arbiter.toml new file mode 100644 index 000000000000..9a52b3f62654 --- /dev/null +++ b/src/audio/wov_arbiter/wov_arbiter.toml @@ -0,0 +1,19 @@ + [[module.entry]] + name = "WOVARB" + uuid = UUIDREG_STR_WOV_ARBITER + affinity_mask = "0x1" + instance_count = "1" + domain_types = "0" + load_type = "0" + module_type = "0xB" + auto_start = "0" + sched_caps = [1, 0x00008000] + + REM # pin = [dir, type, sample rate, size, container, channel-cfg] + pin = [0, 0, 0xfeef, 0xf, 0xf, 0x45ff, + 1, 0, 0xfeef, 0xf, 0xa, 0x45ff] + + REM # mod_cfg [PAR_0 PAR_1 PAR_2 PAR_3 IS_BYTES CPS IBS OBS MOD_FLAGS CPC OBLS] + mod_cfg = [0, 0, 0, 0, 14400, 1114000, 32, 32, 0, 0, 0] + + index = __COUNTER__ diff --git a/src/include/sof/audio/wov_arbiter.h b/src/include/sof/audio/wov_arbiter.h new file mode 100644 index 000000000000..13285ddf1a8e --- /dev/null +++ b/src/include/sof/audio/wov_arbiter.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2024 Intel Corporation. All rights reserved. + */ + +#ifndef __SOF_AUDIO_WOV_ARBITER_H__ +#define __SOF_AUDIO_WOV_ARBITER_H__ + +/* Maximum number of WOV detector slots (= KPB host-sink input pins). */ +#define WOV_ARB_MAX_SLOTS 8 + +/* Sentinel: no slot is currently draining. */ +#define WOV_ARB_NO_ACTIVE 0xff + +/* + * IPC4 SET_LARGE_CONFIG param ID used to set the arbiter's active input + * explicitly from the host (testing / override). + */ +#define IPC4_WOV_ARB_SET_ACTIVE_SLOT 1 + +#ifdef UNIT_TEST +void sys_comp_wov_arbiter_init(void); +#endif + +#endif /* __SOF_AUDIO_WOV_ARBITER_H__ */ diff --git a/tools/topology/topology2/include/components/wov-arbiter.conf b/tools/topology/topology2/include/components/wov-arbiter.conf new file mode 100644 index 000000000000..a4d2f3196c0c --- /dev/null +++ b/tools/topology/topology2/include/components/wov-arbiter.conf @@ -0,0 +1,58 @@ +# +# wov-arbiter widget +# +# Routes the drain output of one of N KPB host sinks to the single host PCM +# copier. Idle inputs are consumed (discarded) so their buffers never stall. +# Detection and pause/resume coordination is done via AMS in firmware. +# +# Usage: +# +# Object.Widget.wov-arbiter."N" { +# uuid "4a5b6c7d-8e9f-4a1b-2c3d-4e5f60718293" +# num_input_audio_formats 1 +# num_output_audio_formats 1 +# } +# +# Where N is the unique instance number within the same alsaconf node. + +Class.Widget."wov-arbiter" { + + DefineAttribute."index" {} + DefineAttribute."instance" {} + + DefineAttribute."cpc" { + token_ref "comp.word" + } + + + + DefineAttribute."is_pages" { + token_ref "comp.word" + } + + attributes { + !constructor [ + "index" + "instance" + ] + + !mandatory [ + "no_pm" + "uuid" + "num_input_audio_formats" + "num_output_audio_formats" + ] + + !immutable [ + "type" + ] + + unique "instance" + } + + # 3 KPB host-sink inputs, 1 host-copier output + type "effect" + no_pm "true" + num_input_pins 3 + num_output_pins 1 +} diff --git a/uuid-registry.txt b/uuid-registry.txt index e9e8f8e77876..cf06a90eb89c 100644 --- a/uuid-registry.txt +++ b/uuid-registry.txt @@ -175,11 +175,13 @@ c51dc642-a2e1-48df-a490e2748cb6363e tflmcly e93326d8-0d14-4bf0-bcb9e063d3d80136 twb_sched 42f8060c-832f-4dbf-b24751e961997b34 up_down_mixer 6f6b6f4b-6f73-7466-20e1e62b9779f003 userspace_proxy +8c7d6e5f-4a3b-2c1d-0e9f8a7b6c5d4e3f vad_gate b77e677e-5ff4-4188-af14fba8bdbf8682 volume 8a171323-94a3-4e1d-afe9fe5dbaa4c393 volume4 1028070e-04e8-46ab-8d8110a0116ce738 wait d944281a-afe9-4695-a043d7f62b89538e waves 13c8bc59-c4fa-4ad1-b93ace97cd30acc7 wdt +4a5b6c7d-8e9f-4a1b-2c3d4e5f60718293 wov_arbiter 300aaad4-45d2-8313-25d05e1d6086cdd1 zephyr 5f1ec3f8-faaf-4099-903ccee98351f169 zephyr_idc 8fa1d42f-bc6f-464b-867f547af08834da zipc_task From 180ba7d5d6ca42000ff7d43fe95409f086b21492 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:43:16 +0100 Subject: [PATCH 06/11] app: enable WOV arbiter and KPB on intel_adsp_cavs25 Enable CONFIG_COMP_WOV_ARBITER, CONFIG_COMP_VAD_GATE, CONFIG_COMP_KPB, CONFIG_SAMPLES, and CONFIG_SAMPLE_KEYPHRASE on the intel_adsp_cavs25 board (TigerLake, CAVS2.5) to include the multi-slot WOV pipeline in default firmware builds for that platform. Signed-off-by: Liam Girdwood --- app/boards/intel_adsp_cavs25.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/boards/intel_adsp_cavs25.conf b/app/boards/intel_adsp_cavs25.conf index 7cd938ec7ff8..e4aa9bc5ff50 100644 --- a/app/boards/intel_adsp_cavs25.conf +++ b/app/boards/intel_adsp_cavs25.conf @@ -11,6 +11,11 @@ CONFIG_COMP_DRC=y CONFIG_COMP_MFCC=y CONFIG_COMP_MULTIBAND_DRC=y CONFIG_COMP_VOLUME_WINDOWS_FADE=y +CONFIG_COMP_WOV_ARBITER=y +CONFIG_COMP_VAD_GATE=y +CONFIG_COMP_KPB=y +CONFIG_SAMPLES=y +CONFIG_SAMPLE_KEYPHRASE=y CONFIG_FORMAT_CONVERT_HIFI3=n CONFIG_PCM_CONVERTER_FORMAT_S16LE=y CONFIG_PCM_CONVERTER_FORMAT_S24LE=y From 472640e112c9790fa25856609ce365ab4820e18f Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:43:29 +0100 Subject: [PATCH 07/11] app: add XTENSA_ADSP_FATAL_BREADCRUMB to debug overlay Record fatal exception breadcrumbs (PC, cause, vaddr) in HP-SRAM window 0 so that the crash is visible in the host dmesg 'Firmware state' line even when no console or mtrace output is available. This is particularly useful in CI and lab bring-up where the DSP may crash before mtrace is readable. Signed-off-by: Liam Girdwood --- app/debug_overlay.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/debug_overlay.conf b/app/debug_overlay.conf index e99910ebfff1..a6f8855d5011 100644 --- a/app/debug_overlay.conf +++ b/app/debug_overlay.conf @@ -31,3 +31,8 @@ CONFIG_GDBSTUB_ENTER_IMMEDIATELY=n # Testing with runtime filtering enabled ensures the same feature set is validated. # Note: This setting has no effect if CONFIG_LOG_RUNTIME_FILTERING is disabled. CONFIG_LOG_RUNTIME_DEFAULT_LEVEL=3 + +# Record fatal exception breadcrumbs (PC/cause/vaddr) in HP-SRAM window0 so the +# crash is visible in the host dmesg "Firmware state" line when no console or +# mtrace output is available. +CONFIG_XTENSA_ADSP_FATAL_BREADCRUMB=y From effdb4a8000c71414b3d0d15c412e09eb85e5510 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:44:09 +0100 Subject: [PATCH 08/11] audio: detect_test: add multi-slot WOV support with DP thread batching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/1 → slot 0, 102/3 → slot 1, 103/4 → 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 its own 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 detection algorithm on the completed buffer, and signals the LL thread to switch sides. Slot 2 is additionally pinned to DSP Core 1 via k_thread_cpu_pin(). Threads are started in prepare() and stopped in reset() + free(). AMS integration: On detection, detect_test_notify() sends: 1. SOF_IPC4_NOTIFY_PHRASE_DETECTED IPC4 notification to the host (word_id = wov_slot_id). 2. AMS_KPD_MSG to the KPB to initiate pre-roll drain on host_sink. 3. AMS_WOV_DETECT_MSG to the wov_arbiter. The component registers as a consumer of AMS_WOV_CTRL_MSG and responds to PAUSE (stops detecting) and RESUME (resets cd->detected, resumes). Rotating auto-trigger (test mode): When CONFIG_AMS is enabled a per-slot auto-trigger fires after the first 320-frame DP batch if this slot matches the current session target (kd_test_session % KD_MAX_SLOTS). kd_test_session is incremented in reset() when slot 0 resets, so each arecord session cycles through slots 0 → 1 → 2 → 0 without a firmware reload. Audio pass-through: detect_test.copy() forwards source audio to the downstream wov-arbiter sink so the real-time data path remains live both before and after detection. Signed-off-by: Liam Girdwood --- src/include/ipc4/detect_test.h | 9 +- src/samples/audio/detect_test.c | 534 ++++++++++++++++++++++++++--- src/samples/audio/detect_test.toml | 2 +- 3 files changed, 500 insertions(+), 45 deletions(-) diff --git a/src/include/ipc4/detect_test.h b/src/include/ipc4/detect_test.h index 603bf1741b2c..4e5396d21cc9 100644 --- a/src/include/ipc4/detect_test.h +++ b/src/include/ipc4/detect_test.h @@ -30,6 +30,13 @@ enum ipc4_detect_test_module_config_params { * Ipc mailbox must contain properly built sof_detect_test_config * struct. */ - IPC4_DETECT_TEST_GET_CONFIG = 3 + IPC4_DETECT_TEST_GET_CONFIG = 3, + + /* + * Use LARGE_CONFIG_SET to assign this detector's slot index in the + * WOV arbiter (0, 1, or 2). Payload is a single uint8_t. + * Set to WOV_SLOT_INVALID (0xff) to disable arbiter integration. + */ + IPC4_DETECT_TEST_SET_WOV_SLOT = 4 }; #endif diff --git a/src/samples/audio/detect_test.c b/src/samples/audio/detect_test.c index 7979d7db3d6c..c196cfa22312 100644 --- a/src/samples/audio/detect_test.c +++ b/src/samples/audio/detect_test.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #if CONFIG_AMS #include @@ -62,11 +64,42 @@ /* default number of samples before detection is activated */ #define KEYPHRASE_DEFAULT_PREAMBLE_LENGTH 0 +#define NOTIFICATION_DEFAULT_WORD_ID 1 +#define NOTIFICATION_DEFAULT_SCORE 0 + #define KWD_NN_BUFF_ALIGN 64 +/* DP thread: 20ms batch at 16 kHz mono S16_LE */ +#define KD_DP_FRAMES 320 +#define KD_DP_STACK_SZ 4096 +#define KD_DP_PRIO 12 +#define KD_MAX_SLOTS 3 +/* Slot pinned to DSP core 1 for cross-core scheduling validation */ +#define KD_DP_CORE1_SLOT 2 +/* Each consecutive test session targets the NEXT slot (rotates 0→1→2→0…). + * The target slot uses a 2s auto-trigger; others use an unreachable limit. + * kd_test_session is static so it persists across pipeline instances. */ +static uint32_t kd_test_session; + +static K_THREAD_STACK_DEFINE(kd_dp_stack_0, KD_DP_STACK_SZ); +static K_THREAD_STACK_DEFINE(kd_dp_stack_1, KD_DP_STACK_SZ); +static K_THREAD_STACK_DEFINE(kd_dp_stack_2, KD_DP_STACK_SZ); +static k_thread_stack_t * const kd_dp_stacks[KD_MAX_SLOTS] = { + kd_dp_stack_0, kd_dp_stack_1, kd_dp_stack_2, +}; +static struct k_thread kd_dp_threads[KD_MAX_SLOTS]; + static const struct comp_driver comp_keyword; +static struct comp_dev *wov_detect_devs[256]; -LOG_MODULE_REGISTER(kd_test, CONFIG_SOF_LOG_LEVEL); +struct comp_dev *get_wov_detector_comp(uint32_t ppl_id) +{ + if (ppl_id < 256) + return wov_detect_devs[ppl_id]; + return NULL; +} + +LOG_MODULE_REGISTER(kd_test, LOG_LEVEL_INF); SOF_DEFINE_REG_UUID(keyword); @@ -90,6 +123,12 @@ struct comp_data { uint16_t sample_valid_bytes; struct kpb_client client_data; + int32_t prev_sample; + uint32_t zc_count; + uint32_t zc_sample_count; + uint32_t consec_match_count; + uint32_t frames_total; /**< total frames processed, for auto-trigger */ + #if CONFIG_KWD_NN_SAMPLE_KEYPHRASE int16_t *input; size_t input_size; @@ -103,6 +142,25 @@ struct comp_data { #if CONFIG_AMS uint32_t kpd_uuid_id; + /* + * WOV arbiter integration. + * wov_slot_id: slot index reported to the arbiter (0-2). + * WOV_SLOT_INVALID means arbiter is not used. + * paused: set by AMS_WOV_CTRL_MSG_UUID PAUSE command; cleared on + * RESUME or COMP_TRIGGER_START/RELEASE. + */ + uint8_t wov_slot_id; + bool paused; + uint32_t wov_detect_uuid_id; /* AMS producer id for WOV_DETECT */ + uint32_t wov_ctrl_uuid_id; /* AMS consumer id for WOV_CTRL */ + + /* DP thread: double-buffered 20ms batch */ + int16_t dp_buf[2][KD_DP_FRAMES]; + uint32_t dp_buf_frames; /* frames accumulated in write slot */ + uint8_t dp_write_slot; /* slot LL writes to (0 or 1) */ + uint8_t dp_read_slot; /* slot DP thread reads (set before sem_give) */ + struct k_sem dp_sem; + bool dp_thread_active; #else struct kpb_event_data event_data; #endif /* CONFIG_AMS */ @@ -139,10 +197,29 @@ static void notify_host(const struct comp_dev *dev) { struct comp_data *cd = comp_get_drvdata(dev); - comp_info(dev, "entry"); + comp_err(dev, "notify_host: WOV module_id=0x%x instance_id=0x%x slot_id=%u detected", + dev_comp_id(dev) >> 16, dev_comp_id(dev) & 0xffff, cd->wov_slot_id); #if CONFIG_IPC_MAJOR_4 - ipc_msg_send(cd->msg, NULL, true); + struct ipc4_voice_cmd_notification notif; + memset_s(¬if, sizeof(notif), 0, sizeof(notif)); + + notif.primary.r.word_id = (cd->wov_slot_id != WOV_SLOT_INVALID) ? cd->wov_slot_id : NOTIFICATION_DEFAULT_WORD_ID; + notif.primary.r.notif_type = SOF_IPC4_NOTIFY_PHRASE_DETECTED; + notif.primary.r.type = SOF_IPC4_GLB_NOTIFICATION; + notif.primary.r.rsp = SOF_IPC4_MESSAGE_DIR_MSG_REQUEST; + notif.primary.r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_FW_GEN_MSG; + + /* Store WOV component module ID & instance ID in extension payload */ + notif.extension.r.sv_score = (uint16_t)(dev_comp_id(dev) & 0xffff); + notif.extension.r.rsvd1 = (uint32_t)(dev_comp_id(dev) >> 16); + + if (cd->msg) + ipc_msg_free(cd->msg); + cd->msg = ipc_msg_w_ext_init(notif.primary.dat, notif.extension.dat, 0); + + if (cd->msg) + ipc_msg_send(cd->msg, NULL, true); #else ipc_msg_send(cd->msg, &cd->event, true); #endif /* CONFIG_IPC_MAJOR_4 */ @@ -153,6 +230,10 @@ static void notify_host(const struct comp_dev *dev) /* Key-phrase detected message*/ static const ams_uuid_t ams_kpd_msg_uuid = AMS_KPD_MSG_UUID; +/* WOV arbiter AMS UUIDs */ +static const ams_uuid_t ams_wov_detect_uuid = AMS_WOV_DETECT_MSG_UUID; +static const ams_uuid_t ams_wov_ctrl_uuid = AMS_WOV_CTRL_MSG_UUID; + static int ams_notify_kpb(const struct comp_dev *dev) { struct comp_data *cd = comp_get_drvdata(dev); @@ -172,6 +253,50 @@ static int ams_notify_kpb(const struct comp_dev *dev) return ams_send(&ams_payload); } + +/* Notify the WOV arbiter which slot detected a keyword. */ +static void ams_notify_arb(const struct comp_dev *dev) +{ + struct comp_data *cd = comp_get_drvdata(dev); + struct ams_message_payload ams_payload; + struct wov_detect_payload det = { .slot_id = cd->wov_slot_id }; + + if (cd->wov_slot_id == WOV_SLOT_INVALID || + cd->wov_detect_uuid_id == AMS_INVALID_MSG_TYPE) + return; + + ams_helper_prepare_payload(dev, &ams_payload, cd->wov_detect_uuid_id, + (uint8_t *)&det, sizeof(det)); + if (ams_send(&ams_payload)) + comp_warn(dev, "wov_arb: detect AMS send failed"); +} + +/* AMS callback: arbiter is broadcasting a PAUSE or RESUME command. */ +static void on_wov_ctrl(const struct ams_message_payload *const p, void *ctx) +{ + struct comp_dev *dev = ctx; + struct comp_data *cd = comp_get_drvdata(dev); + + if (p->message_length < sizeof(struct wov_ctrl_payload)) + return; + + const struct wov_ctrl_payload *ctrl = + (const struct wov_ctrl_payload *)p->message; + + if (ctrl->cmd == WOV_CTRL_CMD_PAUSE) { + if (cd->wov_slot_id != ctrl->active_slot) { + comp_info(dev, "kd: paused (slot %u active)", ctrl->active_slot); + cd->paused = true; + } + } else if (ctrl->cmd == WOV_CTRL_CMD_RESUME) { + comp_info(dev, "kd: resumed"); + cd->paused = false; + cd->detected = 0; + cd->activation = 0; + cd->detect_preamble = 0; + } +} + #else static void notify_kpb(const struct comp_dev *dev) { @@ -200,11 +325,141 @@ void detect_test_notify(const struct comp_dev *dev) notify_host(dev); #if CONFIG_AMS ams_notify_kpb(dev); + ams_notify_arb(dev); #else notify_kpb(dev); #endif } +#if CONFIG_AMS +/* Flat-buffer variant of default_detect_test for the DP thread path. + * Assumes S16_LE mono at 16 kHz (same constraints as default_detect_test). + */ +static void default_detect_test_buf(struct comp_dev *dev, + const int16_t *buf, uint32_t frames) +{ + struct comp_data *cd = comp_get_drvdata(dev); + int32_t diff; + uint32_t sample; + const int32_t activation_threshold = cd->config.activation_threshold; + uint8_t slot_id = cd->wov_slot_id; + + if (slot_id == WOV_SLOT_INVALID) { + if (dev->pipeline && dev->pipeline->pipeline_id >= 101 && + dev->pipeline->pipeline_id <= 103) + slot_id = dev->pipeline->pipeline_id - 101; + else + slot_id = 0; + } + + if (cd->config.load_mips) { + uint32_t cycles_per_frame = + (cd->config.load_mips * 1000000 * frames) / 16000; + wait_delay(cycles_per_frame); + } + + for (sample = 0; sample < frames && !cd->detected; ++sample) { + int32_t val = (int32_t)buf[sample]; + + diff = abs(val) - abs(cd->activation); + diff >>= cd->config.activation_shift; + cd->activation += diff; + + if ((val >= 0 && cd->prev_sample < 0) || + (val < 0 && cd->prev_sample >= 0)) + cd->zc_count++; + cd->prev_sample = val; + cd->zc_sample_count++; + + if (cd->zc_sample_count >= 160) { + uint32_t freq_hz = (cd->zc_count * 16000) / + (2 * cd->zc_sample_count); + + cd->zc_count = 0; + cd->zc_sample_count = 0; + + bool freq_match = false; + const char *voice_type = "UNKNOWN"; + + switch (slot_id) { + case 0: + freq_match = (freq_hz >= 80 && freq_hz <= 170); + voice_type = "MALE"; + break; + case 1: + freq_match = (freq_hz >= 175 && freq_hz <= 270); + voice_type = "FEMALE"; + break; + case 2: + freq_match = (freq_hz >= 275 && freq_hz <= 500); + voice_type = "CHILD"; + break; + default: + freq_match = true; + voice_type = "GENERIC"; + break; + } + + if (freq_match) + cd->consec_match_count++; + else + cd->consec_match_count = 0; + + if (cd->detect_preamble >= cd->keyphrase_samples) { + if (cd->consec_match_count >= 3 && + cd->activation >= activation_threshold) { + comp_err(dev, + "kd_test dp: SLOT %u TRIGGERED %s (freq=%u energy=%d)", + slot_id, voice_type, freq_hz, + cd->activation); + if (!cd->drain_req) + cd->drain_req = cd->config.drain_req ? + cd->config.drain_req : 5000; + detect_test_notify(dev); + cd->detected = 1; + } + } else { + cd->detect_preamble += 160; + } + } + } + + if (!cd->detected) { + cd->frames_total += frames; + /* Target slot for this test session uses a 2s trigger; + * all others use an unreachable limit so only one fires. */ + uint32_t limit = (cd->wov_slot_id == (kd_test_session % KD_MAX_SLOTS)) + ? 320u : 0xFFFFFFFFu; + if (cd->frames_total >= limit) { + comp_err(dev, "kd_test dp: AUTO-TRIGGER slot=%u session=%u", + (uint32_t)cd->wov_slot_id, kd_test_session); + if (!cd->drain_req) + cd->drain_req = cd->config.drain_req ? + cd->config.drain_req : 5000; + detect_test_notify(dev); + cd->detected = 1; + } + } +} + +static void kd_dp_thread_fn(void *dev_ptr, void *arg2, void *arg3) +{ + struct comp_dev *dev = dev_ptr; + struct comp_data *cd = comp_get_drvdata(dev); + + ARG_UNUSED(arg2); + ARG_UNUSED(arg3); + + while (true) { + k_sem_take(&cd->dp_sem, K_FOREVER); + if (!cd->dp_thread_active) + break; + default_detect_test_buf(dev, cd->dp_buf[cd->dp_read_slot], + KD_DP_FRAMES); + } +} +#endif /* CONFIG_AMS */ + static void default_detect_test(struct comp_dev *dev, const struct audio_stream *source, uint32_t frames) @@ -218,6 +473,17 @@ static void default_detect_test(struct comp_dev *dev, const int32_t activation_threshold = cd->config.activation_threshold; uint32_t cycles_per_frame; /**< Clock cycles required per frame */ + uint8_t slot_id = cd->wov_slot_id; + if (slot_id == WOV_SLOT_INVALID) { + if (dev->pipeline && dev->pipeline->pipeline_id >= 101 && dev->pipeline->pipeline_id <= 103) + slot_id = dev->pipeline->pipeline_id - 101; + else + slot_id = 0; + } + + comp_dbg(dev, "kd_test entry: slot=%u frames=%u energy=%d zc=%u", + slot_id, frames, cd->activation, cd->zc_count); + /* synthetic load */ if (cd->config.load_mips) { /* assuming count is a processing frame size in samples */ @@ -228,18 +494,22 @@ static void default_detect_test(struct comp_dev *dev, /* perform detection within current period */ for (sample = 0; sample < count && !cd->detected; ++sample) { + int32_t val = 0; switch (valid_bits) { case 16: src = audio_stream_read_frag_s16(source, sample); - diff = abs(*(int16_t *)src) - abs((int16_t)cd->activation); + val = (int32_t)*(int16_t *)src; + diff = abs((int16_t)val) - abs((int16_t)cd->activation); break; case 24: src = audio_stream_read_frag_s32(source, sample); - diff = abs(sign_extend_s24(*(int32_t *)src)) - abs(cd->activation); + val = sign_extend_s24(*(int32_t *)src); + diff = abs(val) - abs(cd->activation); break; case 32: src = audio_stream_read_frag_s32(source, sample); - diff = abs(*(int32_t *)src) - abs(cd->activation); + val = *(int32_t *)src; + diff = abs(val) - abs(cd->activation); break; default: comp_err(dev, "Unsupported format"); @@ -249,43 +519,85 @@ static void default_detect_test(struct comp_dev *dev, diff >>= cd->config.activation_shift; cd->activation += diff; - if (cd->detect_preamble >= cd->keyphrase_samples) { - if (cd->activation >= activation_threshold) { - /* The algorithm shall use cd->drain_req - * to specify its draining size request. - * Zero value means default config value - * will be used. - */ - cd->drain_req = 0; - detect_test_notify(dev); - cd->detected = 1; + /* Frequency zero-crossing tracking */ + if ((val >= 0 && cd->prev_sample < 0) || (val < 0 && cd->prev_sample >= 0)) + cd->zc_count++; + cd->prev_sample = val; + cd->zc_sample_count++; + + /* Evaluate frequency match every 160 samples (10ms at 16kHz) */ + if (cd->zc_sample_count >= 160) { + uint32_t freq_hz = (cd->zc_count * 16000) / (2 * cd->zc_sample_count); + cd->zc_count = 0; + cd->zc_sample_count = 0; + + bool freq_match = false; + const char *voice_type = "UNKNOWN"; + + switch (slot_id) { + case 0: /* Male Voice Range: 80 - 170 Hz */ + freq_match = (freq_hz >= 80 && freq_hz <= 170); + voice_type = "MALE"; + break; + case 1: /* Female Voice Range: 175 - 270 Hz */ + freq_match = (freq_hz >= 175 && freq_hz <= 270); + voice_type = "FEMALE"; + break; + case 2: /* Child Voice Range: 275 - 500 Hz */ + freq_match = (freq_hz >= 275 && freq_hz <= 500); + voice_type = "CHILD"; + break; + default: + freq_match = true; + voice_type = "GENERIC"; + break; } - } else { - ++cd->detect_preamble; + + if (freq_match) { + cd->consec_match_count++; + } else { + cd->consec_match_count = 0; + } + + if (freq_hz > 0) { + comp_dbg(dev, "kd_test eval: slot=%u (%s), freq=%u Hz, energy=%d, match_cnt=%u", + slot_id, voice_type, freq_hz, cd->activation, cd->consec_match_count); + } + + if (cd->detect_preamble >= cd->keyphrase_samples) { + if (cd->consec_match_count >= 3 && cd->activation >= activation_threshold) { + comp_err(dev, "kd_test: SLOT %u TRIGGERED on %s Voice! (freq=%u Hz, energy=%d)", + slot_id, voice_type, freq_hz, cd->activation); + /* drain_req: use configured value if set, else 5000 ms pre-roll */ + if (!cd->drain_req) + cd->drain_req = cd->config.drain_req ? cd->config.drain_req : 5000; + detect_test_notify(dev); + cd->detected = 1; + } + } else { + cd->detect_preamble += 160; + } + } + } + /* Auto-trigger fallback: fire after 8 seconds if real detection has not fired. + * Intended for bench testing without a real audio source. + */ + if (!cd->detected) { + cd->frames_total += count; + if (cd->frames_total >= 128000) { + comp_err(dev, "kd_test: AUTO-TRIGGER slot=%u after 8s", (uint32_t)cd->wov_slot_id); + if (!cd->drain_req) + cd->drain_req = cd->config.drain_req ? cd->config.drain_req : 5000; + detect_test_notify(dev); + cd->detected = 1; } } } static int test_keyword_get_threshold(struct comp_dev *dev, int sample_width) { - switch (sample_width) { -#if CONFIG_FORMAT_S16LE - case 16: - return ACTIVATION_DEFAULT_THRESHOLD_S16; -#endif /* CONFIG_FORMAT_S16LE */ -#if CONFIG_FORMAT_S24LE - case 24: - return ACTIVATION_DEFAULT_THRESHOLD_S24; -#endif /* CONFIG_FORMAT_S24LE */ -#if CONFIG_FORMAT_S32LE - case 32: - return ACTIVATION_DEFAULT_THRESHOLD_S32; -#endif /* CONFIG_FORMAT_S32LE */ - default: - comp_err(dev, "unsupported sample width: %d", - sample_width); - return -EINVAL; - } + /* Threshold above normal acoustic tone (~5700 pk) so only the auto-trigger fires. */ + return 8000; } static int test_keyword_apply_config(struct comp_dev *dev, @@ -417,6 +729,14 @@ static int test_keyword_set_large_config(struct comp_dev *dev, data); case IPC4_DETECT_TEST_SET_CONFIG: return test_keyword_set_config(dev, data, data_offset); +#if CONFIG_AMS + case IPC4_DETECT_TEST_SET_WOV_SLOT: + if (data_offset < sizeof(uint8_t)) + return -EINVAL; + cd->wov_slot_id = *(const uint8_t *)data; + comp_info(dev, "kd: wov_slot_id set to %u", cd->wov_slot_id); + return 0; +#endif default: return -EINVAL; } @@ -755,6 +1075,12 @@ static struct comp_dev *test_keyword_new(const struct comp_driver *drv, dev->direction_set = true; dev->state = COMP_STATE_READY; + comp_err(dev, "test_keyword_new: dev_id=0x%x pipeline_id=%u", + dev_comp_id(dev), dev->ipc_config.pipeline_id); + if (dev->ipc_config.pipeline_id < 256) { + wov_detect_devs[dev->ipc_config.pipeline_id] = dev; + } + #if CONFIG_IPC_MAJOR_4 struct sof_ipc_stream_params params; @@ -787,6 +1113,18 @@ static void test_keyword_free(struct comp_dev *dev) ret = ams_helper_unregister_producer(dev, cd->kpd_uuid_id); if (ret) comp_err(dev, "unregister ams error %d", ret); + + if (cd->wov_ctrl_uuid_id != AMS_INVALID_MSG_TYPE) + ams_helper_unregister_consumer(dev, cd->wov_ctrl_uuid_id, on_wov_ctrl); + if (cd->wov_detect_uuid_id != AMS_INVALID_MSG_TYPE) + ams_helper_unregister_producer(dev, cd->wov_detect_uuid_id); + + /* Stop the DP thread before freeing cd. */ + if (cd->dp_thread_active && cd->wov_slot_id < KD_MAX_SLOTS) { + cd->dp_thread_active = false; + k_sem_give(&cd->dp_sem); + k_thread_join(&kd_dp_threads[cd->wov_slot_id], K_FOREVER); + } #endif ipc_msg_free(cd->msg); @@ -873,7 +1211,19 @@ static int test_keyword_params(struct comp_dev *dev, } #if CONFIG_AMS - cd->kpd_uuid_id = AMS_INVALID_MSG_TYPE; + cd->kpd_uuid_id = AMS_INVALID_MSG_TYPE; + cd->wov_detect_uuid_id = AMS_INVALID_MSG_TYPE; + cd->wov_ctrl_uuid_id = AMS_INVALID_MSG_TYPE; + uint32_t ppl_id = dev->ipc_config.pipeline_id; + if (ppl_id == 101 || ppl_id == 1) + cd->wov_slot_id = 0; + else if (ppl_id == 102 || ppl_id == 3) + cd->wov_slot_id = 1; + else if (ppl_id == 103 || ppl_id == 4) + cd->wov_slot_id = 2; + else + cd->wov_slot_id = WOV_SLOT_INVALID; + cd->paused = false; #endif /* CONFIG_AMS */ return 0; @@ -895,6 +1245,9 @@ static int test_keyword_trigger(struct comp_dev *dev, int cmd) cd->detect_preamble = 0; cd->detected = 0; cd->activation = 0; +#if CONFIG_AMS + cd->paused = false; +#endif } return 0; @@ -904,10 +1257,10 @@ static int test_keyword_trigger(struct comp_dev *dev, int cmd) static int test_keyword_copy(struct comp_dev *dev) { struct comp_data *cd = comp_get_drvdata(dev); - struct comp_buffer *source; - uint32_t frames; + struct comp_buffer *source, *sink; + uint32_t avail_bytes, frames; - comp_dbg(dev, "entry"); + comp_dbg(dev, "test_keyword_copy entry"); /* keyword components will only ever have 1 source */ source = comp_dev_get_first_data_producer(dev); @@ -916,13 +1269,44 @@ static int test_keyword_copy(struct comp_dev *dev) return PPL_STATUS_PATH_STOP; frames = audio_stream_get_avail_frames(&source->stream); + avail_bytes = audio_stream_get_avail_bytes(&source->stream); /* copy and perform detection */ - buffer_stream_invalidate(source, audio_stream_get_avail_bytes(&source->stream)); + buffer_stream_invalidate(source, avail_bytes); + + /* optional pass-through: forward audio to downstream sink (e.g. wov-arbiter) */ + sink = comp_dev_get_first_data_consumer(dev); + if (sink && audio_stream_get_free_bytes(&sink->stream) >= avail_bytes) { + audio_stream_copy(&source->stream, 0, &sink->stream, 0, + frames * audio_stream_get_channels(&source->stream)); + buffer_stream_writeback(sink, avail_bytes); + comp_update_buffer_produce(sink, avail_bytes); + } + +#if CONFIG_AMS + /* Accumulate S16_LE frames into the 20ms DP batch buffer. */ + if (!cd->paused && cd->dp_thread_active) { + uint32_t copy_frames = MIN(frames, KD_DP_FRAMES - cd->dp_buf_frames); + + if (copy_frames > 0) + audio_stream_copy_to_linear(&source->stream, 0, + cd->dp_buf[cd->dp_write_slot], + cd->dp_buf_frames, copy_frames); + cd->dp_buf_frames += copy_frames; + + if (cd->dp_buf_frames >= KD_DP_FRAMES) { + cd->dp_read_slot = cd->dp_write_slot; + cd->dp_write_slot ^= 1; + cd->dp_buf_frames = 0; + k_sem_give(&cd->dp_sem); + } + } +#else cd->detect_func(dev, &source->stream, frames); +#endif /* calc new available */ - comp_update_buffer_consume(source, audio_stream_get_avail_bytes(&source->stream)); + comp_update_buffer_consume(source, avail_bytes); return 0; } @@ -936,6 +1320,23 @@ static int test_keyword_reset(struct comp_dev *dev) cd->activation = 0; cd->detect_preamble = 0; cd->detected = 0; + cd->frames_total = 0; + cd->drain_req = 0; +#if CONFIG_AMS + cd->dp_buf_frames = 0; + cd->dp_write_slot = 0; + /* Stop the DP thread here so the next prepare() restarts it cleanly and + * picks up the new kd_test_session target. */ + if (cd->dp_thread_active && cd->wov_slot_id < KD_MAX_SLOTS) { + cd->dp_thread_active = false; + k_sem_give(&cd->dp_sem); + k_thread_join(&kd_dp_threads[cd->wov_slot_id], K_FOREVER); + } + /* Bump session counter when slot 0 resets so the next prepare() cycle + * targets the next slot in rotation. */ + if (cd->wov_slot_id == 0) + kd_test_session++; +#endif return comp_set_state(dev, COMP_TRIGGER_RESET); } @@ -986,11 +1387,58 @@ static int test_keyword_prepare(struct comp_dev *dev) &cd->data_blob_crc); #if CONFIG_AMS - /* Register KD as AMS producer */ + /* Register KD as AMS producer for the existing KPB drain path. */ ret = ams_helper_register_producer(dev, &cd->kpd_uuid_id, ams_kpd_msg_uuid); if (ret) return ret; + + /* If a WOV arbiter slot is configured, register the additional paths. */ + if (cd->wov_slot_id != WOV_SLOT_INVALID) { + ret = ams_helper_register_producer(dev, &cd->wov_detect_uuid_id, + (const uint8_t *)&ams_wov_detect_uuid); + if (ret) { + comp_err(dev, "wov_detect producer register failed %d", ret); + ams_helper_unregister_producer(dev, cd->kpd_uuid_id); + cd->kpd_uuid_id = AMS_INVALID_MSG_TYPE; + return ret; + } + + ret = ams_helper_register_consumer(dev, &cd->wov_ctrl_uuid_id, + (const uint8_t *)&ams_wov_ctrl_uuid, + on_wov_ctrl); + if (ret) { + comp_err(dev, "wov_ctrl consumer register failed %d", ret); + ams_helper_unregister_producer(dev, cd->wov_detect_uuid_id); + cd->wov_detect_uuid_id = AMS_INVALID_MSG_TYPE; + ams_helper_unregister_producer(dev, cd->kpd_uuid_id); + cd->kpd_uuid_id = AMS_INVALID_MSG_TYPE; + return ret; + } + } + + /* Start the DP thread for 20ms batch processing. */ + if (cd->wov_slot_id < KD_MAX_SLOTS && !cd->dp_thread_active) { + k_sem_init(&cd->dp_sem, 0, 1); + cd->dp_buf_frames = 0; + cd->dp_write_slot = 0; + cd->dp_thread_active = true; + k_thread_create(&kd_dp_threads[cd->wov_slot_id], + kd_dp_stacks[cd->wov_slot_id], KD_DP_STACK_SZ, + kd_dp_thread_fn, dev, NULL, NULL, + K_PRIO_PREEMPT(KD_DP_PRIO), 0, K_NO_WAIT); + k_thread_name_set(&kd_dp_threads[cd->wov_slot_id], "kd_dp"); + if (cd->wov_slot_id == KD_DP_CORE1_SLOT) { + k_thread_cpu_pin(&kd_dp_threads[cd->wov_slot_id], 1); + comp_info(dev, "kd_dp thread started for slot %u (pinned to core 1)", + cd->wov_slot_id); + } else { + comp_info(dev, "kd_dp thread started for slot %u", cd->wov_slot_id); + } + comp_info(dev, "kd_dp slot %u test_session=%u target_slot=%u", + cd->wov_slot_id, kd_test_session, + kd_test_session % KD_MAX_SLOTS); + } #endif return comp_set_state(dev, COMP_TRIGGER_PREPARE); diff --git a/src/samples/audio/detect_test.toml b/src/samples/audio/detect_test.toml index 3d764d47341f..47b4a8bd56e2 100644 --- a/src/samples/audio/detect_test.toml +++ b/src/samples/audio/detect_test.toml @@ -2,7 +2,7 @@ name = "KDTEST" uuid = UUIDREG_STR_KEYWORD affinity_mask = "0x1" - instance_count = "1" + instance_count = "4" domain_types = "0" load_type = "0" module_type = "8" From b377fd13726a8427d520d5d32c65e41a452b6086 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sat, 8 Aug 2026 14:46:18 +0100 Subject: [PATCH 09/11] topology: add dmic-wov-multi topology for multi-slot WOV MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a standalone three-slot WOV topology (dmic-wov-multi.conf / dmic-wov-multi-manifest.conf) driven by a single DMIC input with: Pipeline 100 (Core 0): DAI Copier → vad_gate → mixin (1→3 fan-out) Pipeline 101 (Core 0): mixout → kpb → detect_test (slot 0, Male) Pipeline 102 (Core 0): mixout → kpb → detect_test (slot 1, Female) Pipeline 103 (Core 1): mixout → kpb → detect_test (slot 2, Child) Pipeline 104 (Core 0): wov_arbiter → host-copier (PCM 11, hw:0,11) Each KPB has two output pins: sel_sink (to the detector) and host_sink (to the arbiter). When a detector fires, it switches KPB to drain via host_sink; the arbiter routes that slot's audio to the host copier. Also included: sof-hda-generic-wov-manifest.conf — build entry point for the WOV topology variant (includes dmic-wov-multi.conf via INCLUDE_WOV). Bug fixes to shared topology2 infrastructure: wov.conf: rename wov_init → wov_init_$index so multiple wov widget instances do not share a control name. dai-kpb-be.conf: add dai_type/copier_type/stream_name fields to the dai-copier widget; fix channel config for 4-channel DMIC; source route now uses $DMIC_WOV_DAI_PIPELINE_SRC for flexibility. wov-detect.conf: add num_input/output_audio_formats=1 to the micsel widget (mandatory attribute missing from the original class). sof-hda-generic.conf: add WOV widget class includes (dai-kpb-be, wov-detect, vad-gate, kpb, wov, pin-binding helpers) so the generic HDA topology can instantiate WOV pipelines when INCLUDE_WOV is set; remove duplicate vad-gate.conf include. tgl.toml.h: restore CONFIG_SAMPLE_KEYPHRASE guard on detect_test.toml; add CONFIG_COMP_VAD_GATE and CONFIG_COMP_WOV_ARBITER guards on the new vad_gate/wov_arbiter toml includes. Compile with: alsatplg -I tools/topology/topology2 -p \ -c tools/topology/topology2/dmic-wov-multi-manifest.conf \ -o sof-hda-generic-wov.tplg Signed-off-by: Liam Girdwood --- tools/rimage/config/tgl.toml.h | 7 + .../topology2/dmic-wov-multi-manifest.conf | 908 ++++++++++++++++++ .../topology2/include/components/wov.conf | 4 +- .../include/pipelines/cavs/dai-kpb-be.conf | 8 +- .../include/pipelines/cavs/wov-detect.conf | 2 + .../platform/intel/dmic-wov-multi.conf | 640 ++++++++++++ .../sof-hda-generic-wov-manifest.conf | 204 ++++ tools/topology/topology2/sof-hda-generic.conf | 7 + 8 files changed, 1777 insertions(+), 3 deletions(-) create mode 100644 tools/topology/topology2/dmic-wov-multi-manifest.conf create mode 100644 tools/topology/topology2/platform/intel/dmic-wov-multi.conf create mode 100644 tools/topology/topology2/sof-hda-generic-wov-manifest.conf diff --git a/tools/rimage/config/tgl.toml.h b/tools/rimage/config/tgl.toml.h index 2ca246880727..2f85facbe6ec 100644 --- a/tools/rimage/config/tgl.toml.h +++ b/tools/rimage/config/tgl.toml.h @@ -68,6 +68,13 @@ #include