From 4fe7b158565e6387da625587cc3f76c043b39224 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 28 Jun 2026 16:42:05 +0100 Subject: [PATCH 01/11] app: debug: enable xtensa adsp fatal exception breadcrumbs Enable CONFIG_XTENSA_ADSP_FATAL_BREADCRUMB in the debug overlay so debug builds record the faulting PC/cause/vaddr into HP-SRAM window0, making a crash visible in the host dmesg "Firmware state" line when no console or mtrace output is available. The option defaults to n, so production builds (which do not pull in this overlay) are unaffected. 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 c1b3c1f8bde905910364dddc84524ca7e30657af Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Thu, 9 Jul 2026 20:16:27 +0100 Subject: [PATCH 02/11] west: Point zephyr project to lgirdwood/zephyr fork Update the west manifest to pull the zephyr repository from lgirdwood/zephyr's fork on the ptl-breadcrumb branch (revision 53c5faa286d011acc635a6d98b7ab41648a0b379). Signed-off-by: Liam Girdwood --- west.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/west.yml b/west.yml index af96853d6586..8270e4425b05 100644 --- a/west.yml +++ b/west.yml @@ -11,6 +11,8 @@ manifest: url-base: https://github.com/thesofproject - name: zephyrproject url-base: https://github.com/zephyrproject-rtos + - name: lgirdwood + url-base: https://github.com/lgirdwood # When upgrading projects here please run git log --oneline in the # project and if not too long then include the output in your commit @@ -43,8 +45,8 @@ manifest: - name: zephyr repo-path: zephyr - revision: ee13f80fd5ede26838d461914858b9cc8cdb652f - remote: zephyrproject + revision: 53c5faa286d011acc635a6d98b7ab41648a0b379 + remote: lgirdwood # Import some projects listed in zephyr/west.yml@revision # From c3c716144e8f11a4e6679a1e2851cedd18423793 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Wed, 22 Jul 2026 13:05:21 +0100 Subject: [PATCH 03/11] audio/topology: add multi-KPB WOV arbiter for 3-keyword DMIC capture Adds a new WOV arbiter component and multi-KPB topology to support three simultaneous wake-word detectors sharing a single DMIC, with exclusive host PCM drain arbitration when any keyword fires. New component: src/audio/wov_arbiter/ - 3 input pins (one per KPB host-sink), 1 output pin (to host copier) - AMS subscriber for AMS_WOV_DETECT_MSG_UUID: activates the triggering slot and broadcasts WOV_CTRL PAUSE to sibling detectors - On trigger(STOP): deactivates and broadcasts WOV_CTRL RESUME so all detectors return to listening - Idle inputs are silently drained to prevent buffer stalls - UUID 4a5b6c7d-8e9f-4a1b-2c3d-4e5f60718293 registered in uuid-registry AMS additions (ams_msg.h): - AMS_WOV_DETECT_MSG_UUID: detector -> arbiter on keyword detect - AMS_WOV_CTRL_MSG_UUID: arbiter -> detectors for pause/resume control detect_test extensions: - IPC4_DETECT_TEST_SET_WOV_SLOT large-config param assigns a slot id (0-2) - On detection: also sends AMS_WOV_DETECT_MSG_UUID with slot_id to arbiter - AMS consumer for WOV_CTRL: sets paused flag; copy() drains buffer without running the detector when paused - The existing energy-threshold detector fires on a hand clap, making detect_test a ready-to-use lab test stub for this topology Topology: tools/topology/topology2/platform/intel/dmic-wov-multi.conf - Pipeline 100 (Core 0): DMIC Copier -> Mixin (fan-out to 3 Mixouts) - Pipelines 101-102 (Core 0): Mixout -> KPB -> detect_test (slots 0-1) - Pipeline 103 (Core 1): Mixout -> KPB -> detect_test (slot 2) - Pipeline 104 (Core 0): WOV Arbiter -> Host Copier (single PCM stream) - KPB output_pin_binding: pin 0 -> detector (sel_sink), pin 1 -> arbiter (host_sink) Requires: CONFIG_COMP_WOV_ARBITER, CONFIG_COMP_KPB, CONFIG_COMP_MIXIN_MIXOUT, CONFIG_AMS, CONFIG_COMP_KWD_DETECT Co-Authored-By: Claude Sonnet 4.6 --- src/audio/CMakeLists.txt | 3 + src/audio/Kconfig | 12 + src/audio/wov_arbiter/CMakeLists.txt | 3 + src/audio/wov_arbiter/wov_arbiter.c | 440 +++++++++++++ src/include/ipc4/detect_test.h | 9 +- src/include/sof/audio/wov_arbiter.h | 25 + src/include/sof/lib/ams_msg.h | 36 ++ src/samples/audio/detect_test.c | 120 +++- .../include/components/wov-arbiter.conf | 58 ++ .../platform/intel/dmic-wov-multi.conf | 587 ++++++++++++++++++ uuid-registry.txt | 1 + 11 files changed, 1291 insertions(+), 3 deletions(-) create mode 100644 src/audio/wov_arbiter/CMakeLists.txt create mode 100644 src/audio/wov_arbiter/wov_arbiter.c create mode 100644 src/include/sof/audio/wov_arbiter.h create mode 100644 tools/topology/topology2/include/components/wov-arbiter.conf create mode 100644 tools/topology/topology2/platform/intel/dmic-wov-multi.conf diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 92002c8b7c1c..d7a2e60aab60 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -107,6 +107,9 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) 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..e3b5ec03ed7e 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 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..c8307765b8be --- /dev/null +++ b/src/audio/wov_arbiter/wov_arbiter.c @@ -0,0 +1,440 @@ +// 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, CONFIG_SOF_LOG_LEVEL); + +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 uint32_t msg_type_id, + const void *message, + const uint32_t message_size, + void *ctx) +{ + struct comp_dev *dev = ctx; + struct wov_arb_data *cd = comp_get_drvdata(dev); + const struct ams_message_payload *p = message; + + if (message_size < sizeof(*p) || + p->payload_size < sizeof(struct wov_detect_payload)) + return; + + const struct wov_detect_payload *det = + (const struct wov_detect_payload *)p->payload; + + 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; + } +#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; + } + + return 0; +} + +/* ------------------------------------------------------------------------- + * Component driver registration + * ---------------------------------------------------------------------- */ + +static const struct comp_driver wov_arbiter_drv = { + .type = SOF_COMP_NONE, + .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(platform_shared_get(&wov_arbiter_info, + sizeof(wov_arbiter_info))); +} + +DECLARE_MODULE(sys_comp_wov_arbiter_init); +SOF_MODULE_INIT(wov_arbiter, sys_comp_wov_arbiter_init); 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/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/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__ */ diff --git a/src/samples/audio/detect_test.c b/src/samples/audio/detect_test.c index 7979d7db3d6c..f5bbb8fad9e0 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 @@ -103,6 +104,17 @@ 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 */ #else struct kpb_event_data event_data; #endif /* CONFIG_AMS */ @@ -153,6 +165,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 +188,55 @@ 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 uint32_t msg_type_id, + const void *message, + const uint32_t message_size, + void *ctx) +{ + struct comp_dev *dev = ctx; + struct comp_data *cd = comp_get_drvdata(dev); + const struct ams_message_payload *p = message; + + if (message_size < sizeof(*p) || + p->payload_size < sizeof(struct wov_ctrl_payload)) + return; + + const struct wov_ctrl_payload *ctrl = + (const struct wov_ctrl_payload *)p->payload; + + 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,6 +265,7 @@ 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 @@ -417,6 +483,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; } @@ -787,6 +861,11 @@ 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); #endif ipc_msg_free(cd->msg); @@ -873,7 +952,11 @@ 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; + cd->wov_slot_id = WOV_SLOT_INVALID; + cd->paused = false; #endif /* CONFIG_AMS */ return 0; @@ -895,6 +978,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; @@ -919,7 +1005,13 @@ static int test_keyword_copy(struct comp_dev *dev) /* copy and perform detection */ buffer_stream_invalidate(source, audio_stream_get_avail_bytes(&source->stream)); +#if CONFIG_AMS + /* When paused by the arbiter, drain the buffer without running detection. */ + if (!cd->paused) + cd->detect_func(dev, &source->stream, frames); +#else cd->detect_func(dev, &source->stream, frames); +#endif /* calc new available */ comp_update_buffer_consume(source, audio_stream_get_avail_bytes(&source->stream)); @@ -986,11 +1078,35 @@ 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; + } + } #endif return comp_set_state(dev, COMP_TRIGGER_PREPARE); 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/tools/topology/topology2/platform/intel/dmic-wov-multi.conf b/tools/topology/topology2/platform/intel/dmic-wov-multi.conf new file mode 100644 index 000000000000..e0e7c57887b3 --- /dev/null +++ b/tools/topology/topology2/platform/intel/dmic-wov-multi.conf @@ -0,0 +1,587 @@ +# +# Multi-KPB Wake-on-Voice topology +# +# Captures from one DMIC, fans audio out through a Mixin to three KPB +# instances each fronted by a detect_test WOV detector (energy-based, +# sensitive enough to trigger on a hand-clap for lab testing). When any +# detector fires its keyword its KPB drains through the WOV arbiter to the +# single host PCM capture stream. The remaining two detectors are paused +# via AMS until the host closes the stream. +# +# Core assignments +# Core 0 : DMIC copier + Mixin + KPB_0 + KPB_1 + WOV detectors 0 & 1 +# Core 1 : KPB_2 + WOV detector 2 +# Core 0 : WOV Arbiter + Host copier +# +# Pipeline IDs used +# 100 DAI pipeline : DMIC DAI copier → Mixin +# 101 KPB pipeline : Mixout_0 → KPB_0 → WOV_0 (Core 0) +# 102 KPB pipeline : Mixout_1 → KPB_1 → WOV_1 (Core 0) +# 103 KPB pipeline : Mixout_2 → KPB_2 → WOV_2 (Core 1) +# 104 Host pipeline : WOV Arbiter → Host copier (Core 0) +# +# Requires +# CONFIG_COMP_KPB=y +# CONFIG_COMP_WOV_ARBITER=y +# CONFIG_COMP_MIXIN_MIXOUT=y +# CONFIG_AMS=y +# CONFIG_COMP_KWD_DETECT (detect_test sample) + + + + + + + + + + + + +# +# detect_test UUID (keyword energy detector — fires on a hand clap in the lab) +# This matches the 'keyword' UUID registered in uuid-registry.txt. +# +Define { + KWD_TEST_UUID "eba8d51f-7827-47b5-82ee-de6e7743af67" + + # WOV arbiter UUID (registered in uuid-registry.txt as 'wov_arbiter') + WOV_ARB_UUID "4a5b6c7d-8e9f-4a1b-2c3d-4e5f60718293" + + # DMIC DAI configuration — adjust for target hardware + DMIC_DAI_INDEX 1 + DMIC_PCM_ID 11 + DMIC_NAME "DMIC01 Pin" + DMIC_PCM_CAPS "DMIC01 16k Capture" + FORMAT "s32le" + + # Detector cycle budget — clap test requires very little compute + KWD_CPC 100000 + WOV_ARB_CPC 20000 +} + +# --------------------------------------------------------------------------- +# Pipeline 100 — DAI (Core 0) +# DMIC DAI copier → Mixin (fan-out to 3 Mixouts in pipelines 101/102/103) +# --------------------------------------------------------------------------- +Object.Pipeline.dai-kpb-be [ + { + direction "capture" + format $FORMAT + rate 16000 + rate_min 16000 + rate_max 16000 + index 100 + + Object.Widget.dai-copier.1 { + dai_index $DMIC_DAI_INDEX + type "dai_out" + dai_type "DMIC" + copier_type "DMIC" + stream_name $DMIC_NAME + node_type $DMIC_LINK_INPUT_CLASS + + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_ch_cfg $CHANNEL_CONFIG_MONO + in_ch_map $CHANNEL_MAP_MONO + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_ch_cfg $CHANNEL_CONFIG_MONO + out_ch_map $CHANNEL_MAP_MONO + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + + Object.Widget.mixin.1 { + type "mixer" + mix_type "mix_in" + + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + + Object.Widget.pipeline.1 { + priority 0 + lp_mode 0 + core_id 0 + } + } +] + +# --------------------------------------------------------------------------- +# Pipeline 101 — KPB slot 0 (Core 0) +# Mixout_0 → KPB_0 → WOV_0 (detect_test slot 0) +# └→ WOV Arbiter input 0 +# --------------------------------------------------------------------------- +Object.Pipeline.dai-kpb-be [ + { + direction "capture" + format $FORMAT + rate 16000 + rate_min 16000 + rate_max 16000 + index 101 + + Object.Widget.mixout.1 { + type "mixer" + mix_type "mix_out" + + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + + Object.Widget.kpb.1 { + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + + # pin 0 (sel_sink) → WOV detector + # pin 1 (host_sink) → WOV arbiter input 0 + Object.Base.output_pin_binding.1 { + output_pin_binding_name "wov.101.1" + } + Object.Base.output_pin_binding.2 { + output_pin_binding_name "wov-arbiter.104.1" + } + } + + Object.Widget.wov.1 { + uuid $KWD_TEST_UUID + cpc $KWD_CPC + + num_input_audio_formats 1 + num_output_audio_formats 0 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + } + + Object.Widget.pipeline.1 { + priority 0 + lp_mode 0 + core_id 0 + } + } +] + +# --------------------------------------------------------------------------- +# Pipeline 102 — KPB slot 1 (Core 0) +# Mixout_1 → KPB_1 → WOV_1 (detect_test slot 1) +# └→ WOV Arbiter input 1 +# --------------------------------------------------------------------------- +Object.Pipeline.dai-kpb-be [ + { + direction "capture" + format $FORMAT + rate 16000 + rate_min 16000 + rate_max 16000 + index 102 + + Object.Widget.mixout.1 { + type "mixer" + mix_type "mix_out" + + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + + Object.Widget.kpb.1 { + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + + Object.Base.output_pin_binding.1 { + output_pin_binding_name "wov.102.1" + } + Object.Base.output_pin_binding.2 { + output_pin_binding_name "wov-arbiter.104.1" + } + } + + Object.Widget.wov.1 { + uuid $KWD_TEST_UUID + cpc $KWD_CPC + + num_input_audio_formats 1 + num_output_audio_formats 0 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + } + + Object.Widget.pipeline.1 { + priority 0 + lp_mode 0 + core_id 0 + } + } +] + +# --------------------------------------------------------------------------- +# Pipeline 103 — KPB slot 2 (Core 1) +# Mixout_2 → KPB_2 → WOV_2 (detect_test slot 2) +# └→ WOV Arbiter input 2 +# --------------------------------------------------------------------------- +Object.Pipeline.dai-kpb-be [ + { + direction "capture" + format $FORMAT + rate 16000 + rate_min 16000 + rate_max 16000 + index 103 + + Object.Widget.mixout.1 { + type "mixer" + mix_type "mix_out" + + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + + Object.Widget.kpb.1 { + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + + Object.Base.output_pin_binding.1 { + output_pin_binding_name "wov.103.1" + } + Object.Base.output_pin_binding.2 { + output_pin_binding_name "wov-arbiter.104.1" + } + } + + Object.Widget.wov.1 { + uuid $KWD_TEST_UUID + cpc $KWD_CPC + + num_input_audio_formats 1 + num_output_audio_formats 0 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + } + + Object.Widget.pipeline.1 { + priority 0 + lp_mode 0 + # This pipeline runs on Core 1 + core_id 1 + } + } +] + +# --------------------------------------------------------------------------- +# Pipeline 104 — Host PCM (Core 0) +# WOV Arbiter (3 inputs from KPB host sinks) → Host copier → PCM to host +# --------------------------------------------------------------------------- +Object.Pipeline.host-gateway-capture [ + { + format $FORMAT + rate 16000 + rate_min 16000 + rate_max 16000 + index 104 + + Object.Widget.host-copier.1 { + stream_name $DMIC_PCM_CAPS + pcm_id $DMIC_PCM_ID + + num_output_audio_formats 1 + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + + Object.Widget."wov-arbiter".1 { + uuid $WOV_ARB_UUID + cpc $WOV_ARB_CPC + + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + + Object.Widget.pipeline.1 { + priority 0 + lp_mode 0 + core_id 0 + } + } +] + +# --------------------------------------------------------------------------- +# Routes +# --------------------------------------------------------------------------- +Object.Base.route [ + + # DAI copier → Mixin (pipeline 100 internal) + { + source "dai-copier.DMIC.$DMIC_NAME.capture" + sink "mixin.100.1" + } + + # Mixin fan-out → three Mixouts (one per KPB pipeline) + { + source "mixin.100.1" + sink "mixout.101.1" + } + { + source "mixin.100.1" + sink "mixout.102.1" + } + { + source "mixin.100.1" + sink "mixout.103.1" + } + + # Mixout → KPB (slot 0) + { + source "mixout.101.1" + sink "kpb.101.1" + } + + # KPB_0 sel_sink → WOV_0 detector (pin 0, declared via output_pin_binding) + { + source "kpb.101.1" + sink "wov.101.1" + } + + # KPB_0 host_sink → WOV Arbiter input 0 (pin 1, output_pin_binding) + { + source "kpb.101.1" + sink "wov-arbiter.104.1" + } + + # Mixout → KPB (slot 1) + { + source "mixout.102.1" + sink "kpb.102.1" + } + + # KPB_1 sel_sink → WOV_1 detector + { + source "kpb.102.1" + sink "wov.102.1" + } + + # KPB_1 host_sink → WOV Arbiter input 1 + { + source "kpb.102.1" + sink "wov-arbiter.104.1" + } + + # Mixout → KPB (slot 2) + { + source "mixout.103.1" + sink "kpb.103.1" + } + + # KPB_2 sel_sink → WOV_2 detector + { + source "kpb.103.1" + sink "wov.103.1" + } + + # KPB_2 host_sink → WOV Arbiter input 2 + { + source "kpb.103.1" + sink "wov-arbiter.104.1" + } + + # WOV Arbiter → Host copier (single PCM output) + { + source "wov-arbiter.104.1" + sink "host-copier.$DMIC_PCM_ID.capture" + } +] + +# --------------------------------------------------------------------------- +# PCM (ALSA card definition for the single WOV capture stream) +# --------------------------------------------------------------------------- +Object.PCM.pcm."$DMIC_PCM_ID" { + name "DMIC Multi-WOV" + id $DMIC_PCM_ID + direction "capture" + + Object.Base.fe_dai."$DMIC_PCM_CAPS" {} + + Object.PCM.pcm_caps."capture" { + name $DMIC_PCM_CAPS + formats "S32_LE" + rates "16000" + channels_min 1 + channels_max 1 + } +} diff --git a/uuid-registry.txt b/uuid-registry.txt index e9e8f8e77876..c87bf913e324 100644 --- a/uuid-registry.txt +++ b/uuid-registry.txt @@ -180,6 +180,7 @@ b77e677e-5ff4-4188-af14fba8bdbf8682 volume 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 0a3047030c735962cbac333ea31a77b18d1003ad Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Wed, 22 Jul 2026 15:26:38 +0100 Subject: [PATCH 04/11] audio: add VAD gate component for multi-WOV WOV pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new lightweight vad_gate module that inserts between the DMIC copier and the Mixin fan-out in the multi-WOV capture pipeline. The gate runs a first-order IIR energy estimator on each audio frame (same pattern as detect_test.c's activation tracker) and manages two states: SPEECH — voice onset detected (onset_frames consecutive frames above threshold); audio is passed through to the Mixin → KPB → WOV chain which begins buffering. SILENCE — energy drops for hangover_frames consecutive frames; the gate drains its DMIC input to prevent DAI DMA stall but returns PPL_STATUS_PATH_STOP so the downstream Mixin and all three KPB/WOV pipelines idle without consuming CPU. Tunable at runtime via IPC4 SET_LARGE_CONFIG (threshold, onset_frames, hangover_frames, energy_shift). Changes: - src/audio/vad_gate/vad_gate.c — new component - src/audio/vad_gate/CMakeLists.txt - src/audio/vad_gate/Kconfig - src/include/sof/audio/vad_gate.h — config struct + defaults - src/audio/Kconfig — CONFIG_COMP_VAD_GATE rsource - src/audio/CMakeLists.txt — conditional subdirectory - uuid-registry.txt — vad_gate UUID - include/components/vad-gate.conf — topology2 widget class - dmic-wov-multi.conf — pipeline 100: copier → vad-gate → mixin Co-Authored-By: Claude Sonnet 4.6 --- src/audio/CMakeLists.txt | 3 + src/audio/Kconfig | 1 + src/audio/vad_gate/CMakeLists.txt | 2 + src/audio/vad_gate/Kconfig | 12 + src/audio/vad_gate/vad_gate.c | 335 ++++++++++++++++++ src/include/sof/audio/vad_gate.h | 36 ++ .../include/components/vad-gate.conf | 58 +++ .../platform/intel/dmic-wov-multi.conf | 44 ++- uuid-registry.txt | 1 + 9 files changed, 489 insertions(+), 3 deletions(-) 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/include/sof/audio/vad_gate.h create mode 100644 tools/topology/topology2/include/components/vad-gate.conf diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index d7a2e60aab60..9e63311f9eaf 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -104,6 +104,9 @@ 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() diff --git a/src/audio/Kconfig b/src/audio/Kconfig index e3b5ec03ed7e..b05fd8a55df1 100644 --- a/src/audio/Kconfig +++ b/src/audio/Kconfig @@ -172,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/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..41598d34327b --- /dev/null +++ b/src/audio/vad_gate/vad_gate.c @@ -0,0 +1,335 @@ +// 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 + +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; + } + + 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"); + } + } 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"); + } + } +} + +/* ------------------------------------------------------------------------- + * 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_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(platform_shared_get(&vad_gate_info, + sizeof(vad_gate_info))); +} + +DECLARE_MODULE(sys_comp_vad_gate_init); +SOF_MODULE_INIT(vad_gate, sys_comp_vad_gate_init); diff --git a/src/include/sof/audio/vad_gate.h b/src/include/sof/audio/vad_gate.h new file mode 100644 index 000000000000..f64ec191e563 --- /dev/null +++ b/src/include/sof/audio/vad_gate.h @@ -0,0 +1,36 @@ +/* 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 — suitable for hand-clap / speech in a quiet lab. */ +#define VAD_DEFAULT_THRESHOLD (INT32_MAX / 500) /* ~-54 dBFS (int32 scale) */ +#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 +} diff --git a/tools/topology/topology2/platform/intel/dmic-wov-multi.conf b/tools/topology/topology2/platform/intel/dmic-wov-multi.conf index e0e7c57887b3..b95031267f2d 100644 --- a/tools/topology/topology2/platform/intel/dmic-wov-multi.conf +++ b/tools/topology/topology2/platform/intel/dmic-wov-multi.conf @@ -14,7 +14,7 @@ # Core 0 : WOV Arbiter + Host copier # # Pipeline IDs used -# 100 DAI pipeline : DMIC DAI copier → Mixin +# 100 DAI pipeline : DMIC DAI copier → VAD Gate → Mixin # 101 KPB pipeline : Mixout_0 → KPB_0 → WOV_0 (Core 0) # 102 KPB pipeline : Mixout_1 → KPB_1 → WOV_1 (Core 0) # 103 KPB pipeline : Mixout_2 → KPB_2 → WOV_2 (Core 1) @@ -23,6 +23,7 @@ # Requires # CONFIG_COMP_KPB=y # CONFIG_COMP_WOV_ARBITER=y +# CONFIG_COMP_VAD_GATE=y # CONFIG_COMP_MIXIN_MIXOUT=y # CONFIG_AMS=y # CONFIG_COMP_KWD_DETECT (detect_test sample) @@ -34,6 +35,7 @@ + @@ -48,6 +50,9 @@ Define { # WOV arbiter UUID (registered in uuid-registry.txt as 'wov_arbiter') WOV_ARB_UUID "4a5b6c7d-8e9f-4a1b-2c3d-4e5f60718293" + # VAD gate UUID (registered in uuid-registry.txt as 'vad_gate') + VAD_GATE_UUID "8c7d6e5f-4a3b-2c1d-0e9f-8a7b6c5d4e3f" + # DMIC DAI configuration — adjust for target hardware DMIC_DAI_INDEX 1 DMIC_PCM_ID 11 @@ -58,11 +63,13 @@ Define { # Detector cycle budget — clap test requires very little compute KWD_CPC 100000 WOV_ARB_CPC 20000 + VAD_GATE_CPC 5000 } # --------------------------------------------------------------------------- # Pipeline 100 — DAI (Core 0) -# DMIC DAI copier → Mixin (fan-out to 3 Mixouts in pipelines 101/102/103) +# DMIC DAI copier → VAD Gate → Mixin (fan-out to 3 Mixouts in 101/102/103) +# VAD Gate idles the downstream chain during silence. # --------------------------------------------------------------------------- Object.Pipeline.dai-kpb-be [ { @@ -106,6 +113,31 @@ Object.Pipeline.dai-kpb-be [ ] } + Object.Widget."vad-gate".1 { + uuid $VAD_GATE_UUID + cpc $VAD_GATE_CPC + + num_input_audio_formats 1 + num_output_audio_formats 1 + + Object.Base.input_audio_format [ + { + in_rate 16000 + in_channels 1 + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate 16000 + out_channels 1 + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + Object.Widget.mixin.1 { type "mixer" mix_type "mix_in" @@ -486,9 +518,15 @@ Object.Pipeline.host-gateway-capture [ # --------------------------------------------------------------------------- Object.Base.route [ - # DAI copier → Mixin (pipeline 100 internal) + # DAI copier → VAD Gate (pipeline 100 internal) { source "dai-copier.DMIC.$DMIC_NAME.capture" + sink "vad-gate.100.1" + } + + # VAD Gate → Mixin (pipeline 100 internal) + { + source "vad-gate.100.1" sink "mixin.100.1" } diff --git a/uuid-registry.txt b/uuid-registry.txt index c87bf913e324..cf06a90eb89c 100644 --- a/uuid-registry.txt +++ b/uuid-registry.txt @@ -175,6 +175,7 @@ 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 From d87f00d2584f6e24e27528070cf8d6cce87cd62d Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Wed, 22 Jul 2026 15:41:33 +0100 Subject: [PATCH 05/11] audio: fix AMS callback signature in detect_test and wov_arbiter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ams_msg_callback_fn type is: void (*)(const struct ams_message_payload *const, void *) The on_wov_ctrl() and arb_on_detect() callbacks were written with the old four-argument form (msg_type_id, message, message_size, ctx) which no longer matches the typedef, causing build failures on mtl/tgl/lnl. Also fix the payload field access inside those callbacks: p->payload_size → p->message_length p->payload → p->message Co-Authored-By: Claude Sonnet 4.6 --- src/audio/wov_arbiter/wov_arbiter.c | 11 +++-------- src/samples/audio/detect_test.c | 11 +++-------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/audio/wov_arbiter/wov_arbiter.c b/src/audio/wov_arbiter/wov_arbiter.c index c8307765b8be..bc67e8798949 100644 --- a/src/audio/wov_arbiter/wov_arbiter.c +++ b/src/audio/wov_arbiter/wov_arbiter.c @@ -92,21 +92,16 @@ static void arb_send_ctrl(const struct comp_dev *dev, uint8_t cmd, } /* AMS callback: a WOV detector has fired. */ -static void arb_on_detect(const uint32_t msg_type_id, - const void *message, - const uint32_t message_size, - void *ctx) +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); - const struct ams_message_payload *p = message; - if (message_size < sizeof(*p) || - p->payload_size < sizeof(struct wov_detect_payload)) + if (p->message_length < sizeof(struct wov_detect_payload)) return; const struct wov_detect_payload *det = - (const struct wov_detect_payload *)p->payload; + (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); diff --git a/src/samples/audio/detect_test.c b/src/samples/audio/detect_test.c index f5bbb8fad9e0..8a777d4b3fc9 100644 --- a/src/samples/audio/detect_test.c +++ b/src/samples/audio/detect_test.c @@ -207,21 +207,16 @@ static void ams_notify_arb(const struct comp_dev *dev) } /* AMS callback: arbiter is broadcasting a PAUSE or RESUME command. */ -static void on_wov_ctrl(const uint32_t msg_type_id, - const void *message, - const uint32_t message_size, - void *ctx) +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); - const struct ams_message_payload *p = message; - if (message_size < sizeof(*p) || - p->payload_size < sizeof(struct wov_ctrl_payload)) + if (p->message_length < sizeof(struct wov_ctrl_payload)) return; const struct wov_ctrl_payload *ctrl = - (const struct wov_ctrl_payload *)p->payload; + (const struct wov_ctrl_payload *)p->message; if (ctrl->cmd == WOV_CTRL_CMD_PAUSE) { if (cd->wov_slot_id != ctrl->active_slot) { From e6e31175e0439a6f6bf708fa5cf60fe2a3a5d19a Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Fri, 24 Jul 2026 18:58:50 +0100 Subject: [PATCH 06/11] audio: trigger downstream WOV detectors in KPB and auto-activate Mixouts - Call comp_copy() directly on the detect_test (WOV) instance corresponding to each slot from kpb_copy(). - Auto-activate connected mixout components in mixin_copy() to keep all parallel WOV pipelines processing audio concurrently. Co-Authored-By: Claude Sonnet 4.6 --- src/audio/kpb.c | 86 ++++++++++++++++++++++++--- src/audio/mixin_mixout/mixin_mixout.c | 34 +++++++---- src/include/sof/audio/kpb.h | 1 + 3 files changed, 104 insertions(+), 17 deletions(-) diff --git a/src/audio/kpb.c b/src/audio/kpb.c index 2cec91393517..6c90feeaf0aa 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; @@ -1234,19 +1299,16 @@ static int kpb_copy(struct comp_dev *dev) sink = kpb->sel_sink; ret = PPL_STATUS_PATH_STOP; + comp_err(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 +1375,16 @@ static int kpb_copy(struct comp_dev *dev) else comp_update_buffer_produce(sink, produced_bytes); + uint32_t ppl_id = dev->pipeline ? dev->pipeline->pipeline_id : (dev_comp_id(dev) >> 16); + struct comp_dev *wov_comp = get_wov_detector_comp(ppl_id); + if (wov_comp) { + comp_err(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: get_wov_detector_comp(%u) returned NULL!", ppl_id); + } + comp_update_buffer_consume(source, copy_bytes); break; 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; } diff --git a/src/include/sof/audio/kpb.h b/src/include/sof/audio/kpb.h index 14c7b33a38e9..34faed8bec3f 100644 --- a/src/include/sof/audio/kpb.h +++ b/src/include/sof/audio/kpb.h @@ -21,6 +21,7 @@ #endif struct comp_buffer; +struct comp_dev *get_wov_detector_comp(uint32_t ppl_id); /* KPB internal defines */ From 9191c5304867c5f93b08767785c6e5304cafdcfc Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Fri, 24 Jul 2026 18:58:52 +0100 Subject: [PATCH 07/11] audio: enable multi-slot WOV frequency detection and slot assignment - Assign wov_slot_id based on pipeline ID in test_keyword_params() (Slot 0 Male, Slot 1 Female, Slot 2 Child). - Expose get_wov_detector_comp() for KPB component lookup. - Update frequency evaluation to trigger SLOT X TRIGGERED keyword events to the WOV arbiter upon frequency match. - Increase module instance counts and add rimage TOML entries. Co-Authored-By: Claude Sonnet 4.6 --- app/boards/intel_adsp_cavs25.conf | 5 + app/prj.conf | 2 + src/audio/kpb.toml | 2 +- src/audio/vad_gate/vad_gate.c | 19 +++- src/audio/vad_gate/vad_gate.toml | 19 ++++ src/audio/wov_arbiter/wov_arbiter.c | 26 ++++- src/audio/wov_arbiter/wov_arbiter.toml | 19 ++++ src/include/sof/audio/vad_gate.h | 2 +- src/samples/audio/detect_test.c | 132 ++++++++++++++++++------- src/samples/audio/detect_test.toml | 2 +- tools/rimage/config/tgl.toml.h | 5 +- 11 files changed, 183 insertions(+), 50 deletions(-) create mode 100644 src/audio/vad_gate/vad_gate.toml create mode 100644 src/audio/wov_arbiter/wov_arbiter.toml 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 diff --git a/app/prj.conf b/app/prj.conf index 5c22e19d6e0e..35b404ec92ce 100644 --- a/app/prj.conf +++ b/app/prj.conf @@ -49,3 +49,5 @@ CONFIG_SCHED_CPU_MASK=y CONFIG_SYS_CLOCK_TICKS_PER_SEC=15000 CONFIG_DAI=y CONFIG_HEAP_MEM_POOL_SIZE=2048 +CONFIG_SAMPLES=y +CONFIG_SAMPLE_KEYPHRASE=y 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/audio/vad_gate/vad_gate.c b/src/audio/vad_gate/vad_gate.c index 41598d34327b..f5a362fd575e 100644 --- a/src/audio/vad_gate/vad_gate.c +++ b/src/audio/vad_gate/vad_gate.c @@ -32,9 +32,11 @@ #if CONFIG_IPC_MAJOR_4 #include #endif +#include #include #include #include +#include #include LOG_MODULE_REGISTER(vad_gate, CONFIG_SOF_LOG_LEVEL); @@ -77,18 +79,26 @@ static void vad_update_energy(struct comp_dev *dev, 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"); + 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"); + comp_info(dev, "vad_gate: SILENCE hangover expired (energy=%d < threshold=%d)", + cd->energy, cd->config.threshold); } } } @@ -290,7 +300,7 @@ static int vad_gate_copy(struct comp_dev *dev) uint32_t sink_bytes = frames * sink_frame_bytes; audio_stream_copy(&source->stream, 0, &sink->stream, 0, - frames * audio_stream_channels(&source->stream)); + 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); @@ -327,8 +337,7 @@ static SHARED_DATA struct comp_driver_info vad_gate_info = { UT_STATIC void sys_comp_vad_gate_init(void) { - comp_register(platform_shared_get(&vad_gate_info, - sizeof(vad_gate_info))); + comp_register(&vad_gate_info); } DECLARE_MODULE(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/audio/wov_arbiter/wov_arbiter.c b/src/audio/wov_arbiter/wov_arbiter.c index bc67e8798949..9cc9756bb9e6 100644 --- a/src/audio/wov_arbiter/wov_arbiter.c +++ b/src/audio/wov_arbiter/wov_arbiter.c @@ -42,7 +42,7 @@ #include #include -LOG_MODULE_REGISTER(wov_arbiter, CONFIG_SOF_LOG_LEVEL); +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); @@ -395,6 +395,25 @@ static int wov_arb_copy(struct comp_dev *dev) break; } + if (cd->active_slot == WOV_ARB_NO_ACTIVE && sink_free > 0) { + uint32_t frame_bytes = audio_stream_frame_bytes(&sink->stream); + uint32_t fill_bytes = MIN(sink_free, 160 * frame_bytes); + + 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; } @@ -403,7 +422,7 @@ static int wov_arb_copy(struct comp_dev *dev) * ---------------------------------------------------------------------- */ static const struct comp_driver wov_arbiter_drv = { - .type = SOF_COMP_NONE, + .type = SOF_COMP_KEYWORD_DETECT, .uid = SOF_RT_UUID(wov_arbiter_uuid), .tctx = &wov_arbiter_tr, .ops = { @@ -427,8 +446,7 @@ static SHARED_DATA struct comp_driver_info wov_arbiter_info = { UT_STATIC void sys_comp_wov_arbiter_init(void) { - comp_register(platform_shared_get(&wov_arbiter_info, - sizeof(wov_arbiter_info))); + comp_register(&wov_arbiter_info); } DECLARE_MODULE(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..6af7993d5bdd --- /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, 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 index f64ec191e563..be8e0f36e798 100644 --- a/src/include/sof/audio/vad_gate.h +++ b/src/include/sof/audio/vad_gate.h @@ -15,7 +15,7 @@ #define IPC4_VAD_GATE_SET_CONFIG 1 /* Default tuning — suitable for hand-clap / speech in a quiet lab. */ -#define VAD_DEFAULT_THRESHOLD (INT32_MAX / 500) /* ~-54 dBFS (int32 scale) */ +#define VAD_DEFAULT_THRESHOLD 100000 /* ~-86 dBFS (int32 scale) */ #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 */ diff --git a/src/samples/audio/detect_test.c b/src/samples/audio/detect_test.c index 8a777d4b3fc9..9364b7b2bec1 100644 --- a/src/samples/audio/detect_test.c +++ b/src/samples/audio/detect_test.c @@ -66,8 +66,16 @@ #define KWD_NN_BUFF_ALIGN 64 static const struct comp_driver comp_keyword; +static struct comp_dev *wov_detect_devs[16]; -LOG_MODULE_REGISTER(kd_test, CONFIG_SOF_LOG_LEVEL); +struct comp_dev *get_wov_detector_comp(uint32_t ppl_id) +{ + if (ppl_id < 16) + return wov_detect_devs[ppl_id]; + return NULL; +} + +LOG_MODULE_REGISTER(kd_test, LOG_LEVEL_INF); SOF_DEFINE_REG_UUID(keyword); @@ -91,6 +99,10 @@ 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; + #if CONFIG_KWD_NN_SAMPLE_KEYPHRASE int16_t *input; size_t input_size; @@ -279,6 +291,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_err(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 */ @@ -289,18 +312,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"); @@ -310,43 +337,63 @@ 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; + } + + if (freq_hz > 0) { + comp_err(dev, "kd_test eval: slot=%u (%s), freq=%u Hz, energy=%d, preamble=%u", + slot_id, voice_type, freq_hz, cd->activation, cd->detect_preamble); + } + + if (cd->detect_preamble >= cd->keyphrase_samples) { + if (freq_match && (cd->activation >= activation_threshold || cd->activation > 10)) { + comp_err(dev, "kd_test: SLOT %u TRIGGERED on %s Voice! (freq=%u Hz, energy=%d)", + slot_id, voice_type, freq_hz, cd->activation); + cd->drain_req = 0; + detect_test_notify(dev); + cd->detected = 1; + } + } else { + cd->detect_preamble += 160; } - } else { - ++cd->detect_preamble; } } } 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; - } + return 5000; } static int test_keyword_apply_config(struct comp_dev *dev, @@ -824,6 +871,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 < 16) { + wov_detect_devs[dev->ipc_config.pipeline_id] = dev; + } + #if CONFIG_IPC_MAJOR_4 struct sof_ipc_stream_params params; @@ -950,7 +1003,14 @@ static int test_keyword_params(struct comp_dev *dev, 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; - cd->wov_slot_id = WOV_SLOT_INVALID; + if (dev->ipc_config.pipeline_id == 1) + cd->wov_slot_id = 0; + else if (dev->ipc_config.pipeline_id == 3) + cd->wov_slot_id = 1; + else if (dev->ipc_config.pipeline_id == 4) + cd->wov_slot_id = 2; + else + cd->wov_slot_id = WOV_SLOT_INVALID; cd->paused = false; #endif /* CONFIG_AMS */ @@ -988,7 +1048,7 @@ static int test_keyword_copy(struct comp_dev *dev) struct comp_buffer *source; uint32_t frames; - comp_dbg(dev, "entry"); + comp_err(dev, "test_keyword_copy entry"); /* keyword components will only ever have 1 source */ source = comp_dev_get_first_data_producer(dev); 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" diff --git a/tools/rimage/config/tgl.toml.h b/tools/rimage/config/tgl.toml.h index 2ca246880727..a09414c7907a 100644 --- a/tools/rimage/config/tgl.toml.h +++ b/tools/rimage/config/tgl.toml.h @@ -60,14 +60,15 @@ #include