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/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..4e6189ae8149 100644 --- a/src/audio/Kconfig +++ b/src/audio/Kconfig @@ -102,6 +102,17 @@ 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 + 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 @@ -109,6 +120,20 @@ config COMP_KPB Select for KPB component if COMP_KPB +config KPB_MAX_NO_OF_CLIENTS + int "Maximum number of KPB clients" + default 4 + help + Maximum number of simultaneous KPB drain clients (host + WOV detectors). + Increase if more than 4 clients need concurrent KPB access. + +config KPB_MAX_BUFF_TIME + int "KPB history buffer time (ms)" + default 6000 if XCHAL_HW_VERSION_MAJOR >= 300 + default 2100 + help + KPB history ring-buffer length in milliseconds. + config KPB_FORCE_COPY_TYPE_NORMAL bool "KPB force copy type normal" default y @@ -160,6 +185,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/kpb.c b/src/audio/kpb.c index 2cec91393517..cf6700711d1c 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_dbg(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,51 @@ static int kpb_prepare(struct comp_dev *dev) return -ENOMEM; } + struct comp_buffer *sink; + /* Output pin IDs: IPC4_COMP_ID(src_queue, dst_queue) = (dst_queue<<16)|src_queue. + * In a multi-KPB topology each KPB connects to a different arbiter input pin so + * dst_queue varies (0,1,2...) but src_queue is always 0 (sel) or 1 (host). + * Match on src_queue only (lower 16 bits of buf_id). */ + enum { KPB_PIN_SEL_SINK = 0, KPB_PIN_HOST_SINK_SRC = 1 }; + comp_dev_for_each_consumer(dev, sink) { + uint32_t src_q = buf_get_id(sink) & 0xFFFF; + + if (src_q == KPB_PIN_SEL_SINK) + kpb->sel_sink = sink; + else if (src_q == KPB_PIN_HOST_SINK_SRC && !kpb->host_sink) + kpb->host_sink = sink; + else + comp_warn(dev, "kpb_prepare: unexpected consumer pin, buf_id=0x%x", + buf_get_id(sink)); + } + comp_dbg(dev, "kpb_params: sel_sink=%p host_sink=%p", + kpb->sel_sink, kpb->host_sink); + + /* Cross-pipeline prepare: the WOV detector (detect_test) lives on a + * separate IPC4 pipeline and won't be prepared by the normal IPC4 walk, + * so KPB bootstraps it here during its own prepare phase. + */ + 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); + ret = comp_prepare(sink_comp); + if (ret < 0) { + comp_err(dev, "kpb_prepare: cross-pipeline prepare of wov detector failed: %d", ret); + return ret; + } + } + } + + 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 +988,42 @@ static int kpb_prepare(struct comp_dev *dev) } #endif /* CONFIG_IPC_MAJOR_4 */ + /* Fallback: iterate consumers to assign sel_sink and host_sink in order. + * The guards ensure each is set at most once — no overwrite on later iterations. */ + 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); + ret = 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); + if (ret < 0) { + comp_err(dev, "kpb_prepare: cross-pipeline prepare failed: %d", ret); + return ret; + } + } } kpb->sync_draining_mode = true; @@ -981,11 +1065,33 @@ 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); + /* Must transition away from KPB_STATE_RUN before returning so that + * a subsequent kpb_copy() does not see a stale RUN state and + * immediately begin copying before the next prepare completes. */ + 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 +1340,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."); + comp_warn(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)) { @@ -1257,7 +1360,7 @@ static int kpb_copy(struct comp_dev *dev) copy_bytes = audio_stream_get_copy_bytes(&source->stream, &sink->stream); if (!copy_bytes) { - comp_err(dev, "nothing to copy sink->free %u source->avail %u", + comp_warn(dev, "nothing to copy sink->free %u source->avail %u", audio_stream_get_free_bytes(&sink->stream), audio_stream_get_avail_bytes(&source->stream)); ret = PPL_STATUS_PATH_STOP; @@ -1278,7 +1381,7 @@ static int kpb_copy(struct comp_dev *dev) produced_bytes = copy_bytes * kpb->num_of_sel_mic / channels; produced_bytes = ROUND_DOWN(produced_bytes, total_bytes_per_sample); if (!copy_bytes) { - comp_err(dev, "nothing to copy sink->free %u source->avail %u", + comp_warn(dev, "nothing to copy sink->free %u source->avail %u", free, avail); ret = PPL_STATUS_PATH_STOP; @@ -1286,8 +1389,9 @@ static int kpb_copy(struct comp_dev *dev) } kpb_micselect_copy(dev, sink, source, produced_bytes, channels); } - /* Buffer source data internally in history buffer for future - * use by clients. + /* Buffer the FULL multi-channel source frame (copy_bytes, not produced_bytes) + * so all KPB clients get the complete channel-count history, regardless of + * which channels kpb_micselect_copy() forwarded to sel_sink. */ if (copy_bytes <= kpb->hd.buffer_size) { ret = kpb_buffer_data(dev, source, copy_bytes); @@ -1313,6 +1417,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_warn(dev, "kpb_copy: downstream sink_comp returned NULL!"); + } + comp_update_buffer_consume(source, copy_bytes); break; @@ -1607,6 +1720,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 +1768,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 +1887,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,9 +2508,11 @@ static void kpb_reset_history_buffer(struct history_buffer *buff) if (!buff) return; - kpb_clear_history_buffer(buff); + do { + /* Reset to start so no stale data from a prior drain session is + * re-played on the next KPB activation. */ buff->w_ptr = buff->start_addr; buff->r_ptr = buff->start_addr; buff->state = KPB_BUFFER_FREE; 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/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..2b2372d829d7 --- /dev/null +++ b/src/audio/vad_gate/Kconfig @@ -0,0 +1,39 @@ +# 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. + +if COMP_VAD_GATE + +config VAD_GATE_DEFAULT_THRESHOLD + int "VAD gate default energy threshold" + default 0 + help + Default peak energy threshold below which frames are treated as silence. + 0 = bypass (pass-through) — all audio reaches downstream; useful for lab + testing where stimulus is a hand clap. Set to a non-zero value to enable + energy-based gating. + +config VAD_GATE_DEFAULT_ONSET_FRAMES + int "VAD gate default onset frame count" + default 3 + help + Number of consecutive above-threshold frames required before the gate + opens (SPEECH onset). + +config VAD_GATE_DEFAULT_HANGOVER_FRAMES + int "VAD gate default hangover frame count" + default 30 + help + Number of consecutive below-threshold frames required before the gate + closes (SILENCE). + +endif # COMP_VAD_GATE diff --git a/src/audio/vad_gate/vad_gate.c b/src/audio/vad_gate/vad_gate.c new file mode 100644 index 000000000000..a02d49b6ae18 --- /dev/null +++ b/src/audio/vad_gate/vad_gate.c @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// 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 +#include +#include +#include +#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 { + struct ipc4_base_module_cfg base_cfg; + 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). + * data_ptr, buf_start, buf_size describe the circular source buffer so wrap + * is handled without the legacy audio_stream API. */ +static void vad_update_energy(struct comp_dev *dev, + const void *data_ptr, const void *buf_start, + size_t buf_size, uint32_t frames) +{ + struct vad_gate_data *cd = comp_get_drvdata(dev); + const int32_t *ptr = data_ptr; + const int32_t *end = (const int32_t *)((const uint8_t *)buf_start + buf_size); + bool above; + uint32_t i; + + /* First-order IIR: energy += (|sample| - energy) >> shift. + * Higher energy_shift = slower attack and release. */ + for (i = 0; i < frames; i++) { + if (ptr >= end) + ptr = buf_start; + int32_t diff = abs(*ptr) - abs(cd->energy); + cd->energy += diff >> cd->config.energy_shift; + ptr++; + } + + + /* Debounce: require onset_frames consecutive above-threshold periods + * to open the gate and hangover_frames below-threshold to close it. */ + 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, "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, "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, "create"); + + 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; + } + + const struct ipc4_base_module_cfg *base_cfg = spec; + memcpy_s(&cd->base_cfg, sizeof(cd->base_cfg), base_cfg, sizeof(*base_cfg)); + + /* Apply compile-time defaults; host can tune via IPC4 set_large_config. */ + 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); + /* VAD gate sits in a capture chain; pass-through when voice active. */ + 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_dbg(dev, "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, "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_dbg(dev, "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, "cmd=%d", cmd); + return comp_set_state(dev, cmd); +} + +static int vad_gate_params(struct comp_dev *dev, + struct sof_ipc_stream_params *params) +{ + 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; + return comp_verify_params(dev, 0, params); +} + +/* ------------------------------------------------------------------------- + * IPC4 large-config — runtime tuning of threshold, onset, hangover. + * ---------------------------------------------------------------------- */ + +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, "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; +} + +/* ------------------------------------------------------------------------- + * 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_buf = comp_dev_get_first_data_producer(dev); + struct sof_source *src = audio_buffer_get_source(&source_buf->audio_buffer); + const void *data_ptr, *buf_start; + size_t buf_size; + uint32_t frame_bytes, frames, n_bytes; + int ret; + + frame_bytes = source_get_frame_bytes(src); + frames = source_get_data_available(src) / frame_bytes; + if (!frames) + return PPL_STATUS_PATH_STOP; + n_bytes = frames * frame_bytes; + + ret = source_get_data(src, n_bytes, &data_ptr, &buf_start, &buf_size); + if (ret) + return ret; + + vad_update_energy(dev, data_ptr, buf_start, buf_size, frames); + + if (!cd->vad_active) { + /* Drain input to keep DMIC DMA running during silence. */ + source_release_data(src, n_bytes); + return PPL_STATUS_PATH_STOP; + } + + /* VAD active: limit frames to what sink can accept. */ + struct comp_buffer *sink_buf = comp_dev_get_first_data_consumer(dev); + struct sof_sink *snk = audio_buffer_get_sink(&sink_buf->audio_buffer); + void *snk_ptr, *snk_buf_start; + size_t snk_buf_size; + uint32_t snk_frames = sink_get_free_size(snk) / frame_bytes; + + if (snk_frames < frames) { + frames = snk_frames; + n_bytes = frames * frame_bytes; + } + if (!frames) { + source_release_data(src, 0); + return 0; + } + + ret = sink_get_buffer(snk, n_bytes, &snk_ptr, &snk_buf_start, &snk_buf_size); + if (ret) { + source_release_data(src, 0); + return ret; + } + + /* Copy with circular-buffer wrap handling for both source and sink. */ + const uint8_t *sp = data_ptr; + uint8_t *dp = snk_ptr; + size_t src_left = (const uint8_t *)buf_start + buf_size - sp; + size_t snk_left = (uint8_t *)snk_buf_start + snk_buf_size - dp; + size_t todo = n_bytes; + + while (todo) { + size_t chunk = MIN(MIN(src_left, snk_left), todo); + + memcpy_s(dp, chunk, sp, chunk); + sp += chunk; + dp += chunk; + src_left -= chunk; + snk_left -= chunk; + todo -= chunk; + if (!src_left) { + sp = buf_start; + src_left = buf_size; + } + if (!snk_left) { + dp = snk_buf_start; + snk_left = snk_buf_size; + } + } + + source_release_data(src, n_bytes); + sink_commit_buffer(snk, n_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, + .set_large_config = vad_gate_set_large_config, + .get_attribute = vad_gate_get_attribute, + }, +}; + +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/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/README.md b/src/audio/wov_arbiter/README.md new file mode 100644 index 000000000000..622843f57d81 --- /dev/null +++ b/src/audio/wov_arbiter/README.md @@ -0,0 +1,1008 @@ +# Multi-Slot Wake-On-Voice (WOV) Architecture & Arbitration + +## Overview + +The Multi-Slot WOV subsystem lets a single DMIC feed up to 3 concurrent keyword detectors +running on the DSP. Each detector has its own Keyphrase Buffer (KPB) that continuously records +a pre-roll window (6 seconds on TigerLake, 2.1 seconds on other platforms). When any detector +fires the `wov_arbiter` drains that KPB's ring buffer to the single host PCM device and pauses +the other detectors. When the host closes the stream the arbiter resumes all detectors. + +This design keeps host DMA live and eliminates the wakeup latency normally incurred by starting +DMA after detection. + +--- + +## Table of Contents + +1. [System Architecture](#system-architecture) +2. [Signal Processing Flow](#signal-processing-flow) +3. [Arbiter State Machine](#arbiter-state-machine) +4. [SOF Notifier Inter-Module Signaling](#sof-notifier-inter-module-signaling) +5. [Firmware API Reference](#firmware-api-reference) +6. [Linux Host API Reference](#linux-host-api-reference) +7. [Adding a New WOV Algorithm](#adding-a-new-wov-algorithm) +8. [Topology: Build and Deploy](#topology-build-and-deploy) +9. [Build System Configuration](#build-system-configuration) +10. [Testing and Verification](#testing-and-verification) + +--- + +## System Architecture + +### Component Graph + +```mermaid +graph TD + subgraph P100["Pipeline 100 — Capture & Gating (Core 0)"] + DAI["DAI Copier\nHDA Analog\ndai_index=1"] + VAD["vad_gate\n(energy estimator)"] + MIX["mixin\n(1→3 fan-out)"] + DAI --> VAD --> MIX + end + + subgraph P101["Pipeline 101 — Slot 0 (Core 0)"] + MO0["mixout 0"] + K0["kpb 0\n6 s ring buffer"] + D0["detect_test\nSlot 0\n(Male 80–170 Hz)"] + MO0 --> K0 + K0 -- sel_sink --> D0 + end + + subgraph P102["Pipeline 102 — Slot 1 (Core 0)"] + MO1["mixout 1"] + K1["kpb 1\n6 s ring buffer"] + D1["detect_test\nSlot 1\n(Female 175–270 Hz)"] + MO1 --> K1 + K1 -- sel_sink --> D1 + end + + subgraph P103["Pipeline 103 — Slot 2 (Core 1)"] + MO2["mixout 2"] + K2["kpb 2\n6 s ring buffer"] + D2["detect_test\nSlot 2\n(Child 275–500 Hz)"] + MO2 --> K2 + K2 -- sel_sink --> D2 + end + + subgraph P104["Pipeline 104 — Arbitration & Host Capture (Core 0)"] + ARB["wov_arbiter\n(first-wins)"] + HC["host-copier\nPCM 11\n'DMIC Multi-WOV'"] + ARB --> HC + end + + MIX --> MO0 + MIX --> MO1 + MIX --> MO2 + + K0 -- host_sink --> ARB + K1 -- host_sink --> ARB + K2 -- host_sink --> ARB + + D0 -- "Notifier WOV_DETECT\n(slot_id=0)" --> ARB + D1 -- "Notifier WOV_DETECT\n(slot_id=1)" --> ARB + D2 -- "Notifier WOV_DETECT\n(slot_id=2)" --> ARB + ARB -- "Notifier WOV_CTRL\n(PAUSE/RESUME)" --> D0 + ARB -- "Notifier WOV_CTRL\n(PAUSE/RESUME)" --> D1 + ARB -- "Notifier WOV_CTRL\n(PAUSE/RESUME)" --> D2 + + style VAD fill:#2d5a27,stroke:#555 + style ARB fill:#1c4966,stroke:#555 + style D0 fill:#663300,stroke:#555 + style D1 fill:#660033,stroke:#555 + style D2 fill:#003366,stroke:#555 +``` + +### Key Design Points + +| Property | Value | +|---|---| +| Audio format | 16 kHz · 1ch · S16_LE throughout | +| KPB pre-roll (TigerLake) | 6 000 ms (192 000 bytes) | +| KPB pre-roll (other) | 2 100 ms | +| Max arbiter slots | 3 (topology), 8 (header constant) | +| Arbitration policy | First-wins; subsequent detections ignored until RESUME | +| Slot 2 core affinity | DSP Core 1 (cross-core scheduling validation) | +| Host PCM | card 0, device 11 — `hw:0,11` | + +--- + +## Signal Processing Flow + +### LL Thread Path (every 1 ms) + +```mermaid +flowchart LR + HW["HW DMA\n(DMIC interrupt)"] + DAI_COPY["dai_copier.copy()\nPipeline 100"] + VAD_COPY["vad_gate.copy()\nenergy > threshold?"] + MIXIN["mixin.copy()\nduplicate into\n3 ring buffers"] + MIXOUT["mixout_N.copy()"] + KPB_COPY["kpb_N.copy()\nwrite ring buffer\nsel_sink → detect_test"] + DT_COPY["detect_test.copy()\naccumulate 320 frames\ninto ping-pong buffer\nthen k_sem_give()"] + + HW --> DAI_COPY --> VAD_COPY + VAD_COPY -- "energy OK" --> MIXIN + VAD_COPY -- "silence" --> STOP(["PPL_STATUS_PATH_STOP\n(idle pipelines)"]) + MIXIN --> MIXOUT --> KPB_COPY --> DT_COPY +``` + +### DP Thread Path (every 20 ms per slot) + +Each slot has its own `k_thread` at `K_PRIO_PREEMPT(12)`. Slot 2 is additionally pinned to +DSP Core 1 via `k_thread_cpu_pin()`. + +```mermaid +flowchart TD + SEM["k_sem_take()"] + CHECK{dp_thread_active?} + BUF["read dp_buf[read_slot]\n320 × S16_LE samples"] + DETECT["run detection algorithm\n(zero-crossing + energy)"] + FOUND{match?} + NOTIFY["detect_test_notify(dev)\n① IPC4 → host\n② KPB drain (notify_event)\n③ Notifier WOV_DETECT → arbiter"] + AUTOTRIG["auto-trigger\n(test only, limit=320 frames)"] + LOOP(["loop"]) + + SEM --> CHECK + CHECK -- "false" --> EXIT(["thread exits"]) + CHECK -- "true" --> BUF --> DETECT --> FOUND + FOUND -- "yes" --> NOTIFY --> LOOP + FOUND -- "no" --> AUTOTRIG --> LOOP + LOOP --> SEM +``` + +### KPB Drain Sequence (triggered by Notifier) + +```mermaid +flowchart LR + NOTIF_EVT["Notifier WOV_DETECT\n→ arb_on_detect()"] + SWITCH["KPB: switch sel_sink\n→ host_sink"] + DRAIN["drain ring buffer\n(up to 6 s pre-roll)\nto wov_arbiter input buffer"] + LIVE["continue forwarding\nlive DMIC audio"] + + NOTIF_EVT --> SWITCH --> DRAIN --> LIVE +``` + +--- + +## Arbiter State Machine + +```mermaid +stateDiagram-v2 + [*] --> Idle : wov_arb_new() / prepare()\nactive_slot = NO_ACTIVE\nbroadcast RESUME + + Idle --> Active : Notifier WOV_DETECT(slot_id=N)\nactive_slot = N\nbroadcast PAUSE(N) + + Active --> Active : Notifier WOV_DETECT(slot_id=M)\n[first-wins: ignored] + + Active --> Idle : trigger(STOP or PAUSE)\nactive_slot = NO_ACTIVE\nbroadcast RESUME + + Idle --> [*] : wov_arb_free() +``` + +### `wov_arb_copy()` Routing Logic + +| Condition | Active slot buffer | Idle slot buffers | Sink output | +|---|---|---|---| +| `active_slot == NO_ACTIVE` | — | drained and discarded | filled with silence (memset 0) | +| `active_slot == N` | copied frame-aligned to sink | drained and discarded | pre-roll + live audio | + +The first-wins guard in `arb_on_detect()`: + +```c +if (cd->active_slot != WOV_ARB_NO_ACTIVE) { + comp_warn(dev, "wov_arb: slot %u fired but slot %u already active, ignoring", + det->slot_id, cd->active_slot); + return; +} +``` + +--- + +## SOF Notifier Inter-Module Signaling + +The SOF Notifier system (`src/include/sof/lib/notifier.h`) is SOF's intra-DSP +publish/subscribe bus. It works on all platforms (no `CONFIG_AMS` required) and +is already used for KPB client events. Signals are delivered synchronously to +all registered listeners on the calling core. + +### Signal Catalog + +| Notifier ID | Direction | Payload struct | Purpose | +|---|---|---|---| +| `NOTIFIER_ID_WOV_DETECT` | detector → arbiter | `struct wov_detect_notif { uint8_t slot_id; }` | Announce keyword detection | +| `NOTIFIER_ID_WOV_CTRL` | arbiter → all detectors | `struct wov_ctrl_notif { uint8_t cmd; }` | Pause/resume detectors | + +`cmd` values: `WOV_ARB_CMD_PAUSE`, `WOV_ARB_CMD_RESUME` (defined in `wov_arbiter.h`). + +### Full Detect-to-Drain Sequence + +```mermaid +sequenceDiagram + autonumber + participant DMIC as DMIC (HW) + participant KPB as KPB N + participant DET as detect_test (slot N) + participant ARB as wov_arbiter + participant HOST as Host PCM (arecord) + participant OTHER as detect_test (slots ≠ N) + + Note over DMIC,OTHER: Listening state — all slots accumulating pre-roll + + loop Every 1 ms (LL period) + DMIC->>KPB: DAI DMA frames + KPB->>DET: sel_sink copy + end + + loop Every 20 ms (DP batch) + DET->>DET: run algorithm on 320-frame batch + end + + Note over DET,ARB: Keyword detected on slot N + + DET->>HOST: ① IPC4 SOF_IPC4_NOTIFY_PHRASE_DETECTED\n (word_id = slot_id) + DET->>KPB: ② notifier_event(WOV_DETECT) [KPB already wired via kpb_client] + DET->>ARB: ③ notifier_event(NOTIFIER_ID_WOV_DETECT, slot_id=N) + + ARB->>ARB: active_slot = N + ARB->>OTHER: notifier_event(NOTIFIER_ID_WOV_CTRL, cmd=PAUSE) + OTHER->>OTHER: cd->paused = true\n(stops DP batching) + + KPB->>ARB: stream pre-roll (up to 6 s) via host_sink + ARB->>HOST: route slot-N audio to host PCM + + Note over HOST,ARB: Host finishes reading / closes PCM + + HOST->>ARB: trigger STOP (ALSA hw_free / snd_pcm_close) + ARB->>ARB: active_slot = NO_ACTIVE + ARB->>OTHER: notifier_event(NOTIFIER_ID_WOV_CTRL, cmd=RESUME) + ARB->>DET: notifier_event(NOTIFIER_ID_WOV_CTRL, cmd=RESUME) + OTHER->>OTHER: cd->paused = false\ncd->detected = 0\nresumed listening +``` + +--- + +## Firmware API Reference + +### IPC4 Module Parameters (`LARGE_CONFIG_SET`) + +#### `detect_test` — slot assignment via `wov_init_1NN` bytes kcontrol + +Each `detect_test` instance exposes an ALSA bytes TLV kcontrol that configures the +slot assignment at runtime. The topology creates three such kcontrols: + +| ALSA kcontrol name | numid | Target module | Slot | +|---|---|---|---| +| `wov_init_101` | 8 | `wov.101.1` (Core 0) | 0 | +| `wov_init_102` | 9 | `wov.102.1` (Core 0) | 1 | +| `wov_init_103` | 10 | `wov.103.1` (Core 1) | 2 | + +The write must be issued **before** `PREPARE` fires — write the kcontrols +immediately after forking `arecord` into the background, before it has finished +`hw_params`. If the `PREPARE` IPC lands while `wov_slot_id` is still `0xff` +(`WOV_SLOT_INVALID`), the DP thread is not started and the notifier is not +registered for that slot. + +The TLV payload format and a ready-to-use Python helper are in +[Testing and Verification](#testing-and-verification). + +#### `wov_arbiter` — slot count from `nb_input_pins` + +The arbiter reads the number of active slots from `ipc4_base_module_cfg_ext.nb_input_pins` +(set in `wov-arbiter.conf` via `num_input_pins = 3`). + +### Notifier Registration + +A WOV detector must register for `WOV_CTRL` notifications during `prepare()`: + +```c +notifier_register(dev, NULL, NOTIFIER_ID_WOV_CTRL, on_wov_ctrl, 0); +``` + +Unregister in `free()`: + +```c +notifier_unregister(dev, NULL, NOTIFIER_ID_WOV_CTRL); +``` + +### `detect_test_notify(dev)` — Detection Announcement + +Call this from your detection algorithm when a keyword is confirmed: + +```c +void detect_test_notify(const struct comp_dev *dev); +``` + +Internally executes three steps: +1. Sends `SOF_IPC4_NOTIFY_PHRASE_DETECTED` IPC4 notification to host +2. Sends `kpb_client` notifier event to KPB → triggers pre-roll drain on `host_sink` +3. Fires `notifier_event(NOTIFIER_ID_WOV_DETECT)` → `wov_arbiter.arb_on_detect()` activates this slot + +### DP Thread Ping-Pong Buffer Contract + +| Field | Type | Semantics | +|---|---|---| +| `dp_buf[2][320]` | `int16_t` | ping-pong accumulation buffer | +| `dp_buf_frames` | `uint32_t` | frames in write slot (0 to 319) | +| `dp_write_slot` | `uint8_t` | index (0 or 1) LL writes into | +| `dp_read_slot` | `uint8_t` | index DP thread reads from | +| `dp_sem` | `struct k_sem` | binary semaphore, max count = 1 | +| `dp_thread_active` | `bool` | false → thread exits on next wake | + +LL thread gives semaphore after every 320 accumulated frames. +DP thread takes semaphore and processes `dp_buf[dp_read_slot]`. +Missing a give (while thread is still processing) drops that batch silently +(semaphore max count = 1 prevents accumulation). + +--- + +## Linux Host API Reference + +### ALSA Capture + +The WOV audio is exposed as a standard ALSA PCM capture device: + +``` +Card: 0 Device: 11 Name: "DMIC Multi-WOV" +Formats: S16_LE, S24_LE, S32_LE +Rate: 16000 Hz +Channels: 1 (mono) +``` + +Open and capture with `arecord`: + +```bash +arecord -Dhw:0,11 -r 16000 -c 1 -f S16_LE -d 10 /tmp/wov_capture.wav +``` + +Or via libasound in code: + +```c +snd_pcm_t *handle; +snd_pcm_open(&handle, "hw:0,11", SND_PCM_STREAM_CAPTURE, 0); +snd_pcm_set_params(handle, SND_PCM_FORMAT_S16_LE, + SND_PCM_ACCESS_RW_INTERLEAVED, + 1, 16000, 1, 100000 /* 100ms latency */); +``` + +### Voice Detection Notification from Firmware + +When a keyword is detected the firmware sends an IPC4 `SOF_IPC4_NOTIFY_PHRASE_DETECTED` +notification. The SOF kernel driver surfaces this as an ALSA control-change event on a +`SOUNDWIRE_DETECT` or `KWD_DETECT` kcontrol (exact name depends on machine driver). + +To poll for the notification from userspace: + +```c +/* Open ALSA control interface */ +snd_ctl_t *ctl; +snd_ctl_open(&ctl, "hw:0", 0); + +/* Subscribe to events */ +snd_ctl_subscribe_events(ctl, 1); + +/* Block until a control change event arrives */ +snd_ctl_event_t *event; +snd_ctl_event_alloca(&event); +while (snd_ctl_read(ctl, event) >= 0) { + if (snd_ctl_event_get_type(event) == SND_CTL_EVENT_ELEM) { + /* Trigger: start reading from hw:0,11 */ + break; + } +} +``` + +The `word_id` field in the IPC4 notification carries the `wov_slot_id` (0, 1, or 2), +identifying which detector fired. + +### Audio Capture Timing + +Because host DMA is live from the moment `arecord` opens `hw:0,11`, the ALSA +buffer begins filling with `wov_arbiter` output immediately: + +- **Before any trigger**: arbiter writes silence (all-zero samples) +- **On trigger**: arbiter drains the KPB ring buffer (up to 6 s of pre-roll) then + continues forwarding live DMIC audio +- **On STOP**: arbiter returns to silence mode; detectors resume listening + +The host sees a seamless audio stream. The silence-to-audio transition marks the +trigger point in the buffer. + +### IPC4 Module Control via `sof-ctl` + +Override the slot assignment for a `detect_test` instance using the SOF IPC4 +LARGE_CONFIG_SET path (requires `sof-ctl` tool or equivalent hwdep ioctl): + +```bash +# Set detect_test in pipeline 102 to use slot_id = 1 +sof-ctl -c hw:0 -t large_config_set \ + -m -i \ + -p 202 -b 00000000 01 # param_id=202 (SOF_IPC4_BYTES_CONTROL_PARAM_ID), slot_id=1 +``` + +--- + +## Adding a New WOV Algorithm + +The reference implementation (`detect_test.c`) is intentionally simple — it looks for +zero-crossing rate and signal energy in fixed frequency bands. Replace it with any +algorithm by following one of the approaches below. + +### Approach A — Modify `detect_test.c` directly + +This is simplest for prototyping. The DP thread calls `default_detect_test_buf()` on +every 320-frame batch. Replace its body with your algorithm: + +```c +/* src/samples/audio/detect_test.c */ + +static void default_detect_test_buf(struct comp_dev *dev, + const int16_t *samples, uint32_t frames) +{ + struct comp_data *cd = comp_get_drvdata(dev); + + if (cd->detected || cd->paused) + return; + + /* === YOUR ALGORITHM HERE === + * Input: samples — pointer to 'frames' interleaved S16_LE samples + * frames — always KD_DP_FRAMES (320) = 20 ms @ 16 kHz + * Output: call detect_test_notify(dev) when keyword is confirmed. + * =========================== + */ + bool keyword_found = my_algorithm_process(cd->algo_state, samples, frames); + + if (keyword_found) { + comp_err(dev, "KWD detected on slot %u", 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; + } +} +``` + +Store per-slot algorithm state in `struct comp_data` (add fields to the struct). +The slot index is available as `cd->wov_slot_id` (0, 1, or 2) so each KPB pipeline's +detector can hold independent model state. + +### Approach B — New Native SOF Module + +For a production algorithm create a new SOF module following the standard +`src/audio/template/` skeleton and integrate it with the arbiter via SOF notifier: + +**Step 1: Create the module files** + +``` +src/audio/my_kwd/ +├── CMakeLists.txt +├── my_kwd.c +└── my_kwd.h +``` + +**Step 2: Implement the module driver** + +```c +/* src/audio/my_kwd/my_kwd.c */ +#include +#include +#include +#include + +struct my_kwd_data { + uint8_t wov_slot_id; + bool paused; + bool detected; + /* ... algorithm state ... */ +}; + +/* Called by arbiter when a sibling slot fires (NOTIFIER_ID_WOV_CTRL) */ +static void on_wov_ctrl(void *arg, enum notify_id id, void *data) +{ + struct comp_dev *dev = arg; + struct my_kwd_data *cd = comp_get_drvdata(dev); + const struct wov_ctrl_notif *ctrl = data; + + if (ctrl->cmd == WOV_ARB_CMD_PAUSE) + cd->paused = true; + else if (ctrl->cmd == WOV_ARB_CMD_RESUME) { + cd->paused = false; + cd->detected = 0; + } +} + +/* Notify arbiter + KPB + host that this slot fired */ +static void my_kwd_notify(struct comp_dev *dev) +{ + struct my_kwd_data *cd = comp_get_drvdata(dev); + + /* 1. IPC4 notification to host */ + struct ipc4_voice_cmd_notification notif = {}; + notif.primary.r.word_id = cd->wov_slot_id; + notif.primary.r.notif_type = SOF_IPC4_NOTIFY_PHRASE_DETECTED; + notif.primary.r.type = SOF_IPC4_GLB_NOTIFICATION; + /* ... fill in module_id / instance_id ... */ + ipc_msg_send(cd->msg, ¬if, true); + + /* 2. Tell KPB to start draining */ + /* (re-use existing kpb_client notifier path) */ + + /* 3. Tell arbiter slot fired */ + struct wov_detect_notif det = { .slot_id = cd->wov_slot_id }; + notifier_event(dev, NOTIFIER_ID_WOV_DETECT, NOTIFIER_TARGET_CORE_ALL_MASK, + &det, sizeof(det)); +} + +static int my_kwd_prepare(struct comp_dev *dev) +{ + struct my_kwd_data *cd = comp_get_drvdata(dev); + + /* Derive slot from pipeline ID */ + uint32_t ppl = dev->ipc_config.pipeline_id; + cd->wov_slot_id = (ppl == 101 || ppl == 1) ? 0 : + (ppl == 102 || ppl == 3) ? 1 : + (ppl == 103 || ppl == 4) ? 2 : WOV_SLOT_INVALID; + + if (cd->wov_slot_id != WOV_SLOT_INVALID) + notifier_register(dev, NULL, NOTIFIER_ID_WOV_CTRL, on_wov_ctrl, 0); + + return comp_set_state(dev, COMP_TRIGGER_PREPARE); +} + +static int my_kwd_copy(struct comp_dev *dev) +{ + struct my_kwd_data *cd = comp_get_drvdata(dev); + struct comp_buffer *source = comp_dev_get_first_data_producer(dev); + + if (!audio_stream_get_avail(&source->stream)) + return PPL_STATUS_PATH_STOP; + + uint32_t frames = audio_stream_get_avail_frames(&source->stream); + uint32_t avail_b = audio_stream_get_avail_bytes(&source->stream); + buffer_stream_invalidate(source, avail_b); + + /* Pass-through to arbiter input */ + struct comp_buffer *sink = comp_dev_get_first_data_consumer(dev); + if (sink && audio_stream_get_free_bytes(&sink->stream) >= avail_b) { + audio_stream_copy(&source->stream, 0, &sink->stream, 0, + frames * audio_stream_get_channels(&source->stream)); + buffer_stream_writeback(sink, avail_b); + comp_update_buffer_produce(sink, avail_b); + } + + if (!cd->paused && !cd->detected) { + /* Run your algorithm on the frames */ + if (my_algorithm_run(cd, &source->stream, frames)) { + my_kwd_notify(dev); + cd->detected = 1; + } + } + + comp_update_buffer_consume(source, avail_b); + return 0; +} +``` + +**Step 3: Register the UUID** + +Add to `uuid-registry.txt`: + +``` +MY_KWD_UUID_HEX my_kwd +``` + +Add to `src/audio/my_kwd/CMakeLists.txt`: + +```cmake +add_local_sources(sof my_kwd.c) +``` + +Add to `src/audio/CMakeLists.txt`: + +```cmake +if(CONFIG_COMP_MY_KWD) + add_subdirectory(my_kwd) +endif() +``` + +Add `src/audio/Kconfig`: + +```kconfig +config COMP_MY_KWD + bool "My keyword detector" + depends on COMP_KPB && IPC_MAJOR_4 + # no AMS required (uses SOF notifier) +``` + +**Step 4: Update the topology** + +In `dmic-wov-multi.conf`, replace `KWD_TEST_UUID` with the UUID bytes for `my_kwd` +in the `wov.101.1`, `wov.102.1`, `wov.103.1` widgets. + +### Approach C — IADK / LLEXT loadable module + +For algorithms that must ship as separate binaries (third-party IP, updatable +without reflashing), use the SOF IADK module adapter framework. The algorithm +is compiled as an LLEXT `.so` and loaded at runtime from the kernel filesystem. +See `src/audio/module_adapter/README.md` for the full IADK API. + +The notifier signaling path (steps 2 and 3 of `my_kwd_notify`) remains the same; +only the binary delivery mechanism changes. + +### Algorithm State Isolation + +Each pipeline instance (101/102/103) creates its own `comp_dev` with independent +`comp_data`. Detector state is never shared between slots. The `wov_slot_id` field +identifies which pipeline a given instance belongs to: + +``` +Pipeline 101 → wov_slot_id=0 → kd_dp_stack_0 / kd_dp_threads[0] +Pipeline 102 → wov_slot_id=1 → kd_dp_stack_1 / kd_dp_threads[1] +Pipeline 103 → wov_slot_id=2 → kd_dp_stack_2 / kd_dp_threads[2] (Core 1) +``` + +--- + +## Topology: Build and Deploy + +### Topology Source Layout + +``` +tools/topology/topology2/ +├── platform/intel/ +│ └── dmic-wov-multi.conf ← main topology (edit here) +├── include/components/ +│ └── wov-arbiter.conf ← wov_arbiter widget class definition +└── dmic-wov-multi-manifest.conf ← top-level manifest (includes above) +``` + +### Compile Topology to `.tplg` + +From the root of the SOF repository: + +```bash +ALSA_CONFIG_DIR=tools/topology/topology2 \ +alsatplg \ + -I tools/topology/topology2 \ + -p \ + -c tools/topology/topology2/dmic-wov-multi-manifest.conf \ + -o sof-hda-generic-wov.tplg +``` + +Copy the compiled topology to the DUT: + +```bash +scp sof-hda-generic-wov.tplg \ + root@:/lib/firmware/intel/sof-ipc4-tplg/sof-hda-generic-wov.tplg +``` + +Tell the SOF driver which topology to load (edit `/etc/modprobe.d/sof.conf` on the DUT): + +``` +options snd_sof_pci_intel_tgl fw_path=intel/sof-ipc4/tgl \ + tplg_path=intel/sof-ipc4-tplg tplg_filename=sof-hda-generic-wov.tplg +``` + +Or pass directly at `modprobe` time: + +```bash +modprobe snd_sof_pci_intel_tgl \ + tplg_filename=intel/sof-ipc4-tplg/sof-hda-generic-wov.tplg +``` + +### Build Firmware with WOV Arbiter + +Configure a build directory with the required Kconfig options (see +[Build System Configuration](#build-system-configuration)), then build: + +```bash +ninja -C +``` + +Copy the firmware image to the DUT (path varies by platform): + +```bash +# TigerLake example +scp /zephyr/zephyr.ri \ + root@:/lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri +``` + +Reload the driver on the DUT: + +```bash +rmmod snd_sof_pci_intel_tgl && modprobe snd_sof_pci_intel_tgl +``` + +### Topology Configuration Reference + +Key parameters in `dmic-wov-multi.conf`: + +| Constant | Default | Description | +|---|---|---| +| `DMIC_PCM_ID` | `11` | ALSA PCM device index | +| `DMIC_DAI_INDEX` | `1` | HDA DAI instance | +| `KWD_CPC` | `100000` | cycles per chunk for detector widgets | +| `WOV_ARB_CPC` | `20000` | cycles per chunk for arbiter | +| `VAD_GATE_CPC` | `5000` | cycles per chunk for VAD gate | +| `FORMAT` | `s16le` | audio format throughout | + +Slot 2 (`Pipeline 103`) is deliberately placed on Core 1 (`core_id = 1`) to validate +cross-core scheduling. Set all three to `core_id = 0` if a single-core topology is needed. + +### Adding a Fourth Slot + +1. Add `Pipeline 104` (new KPB + detector) following the pattern of pipelines 101–103. + Route the new `wov.104.1 → wov-arbiter.105.1`. +2. Update `wov_arbiter.conf`: set `num_input_pins = 4`. +3. Update `wov_arbiter.h`: `WOV_ARB_MAX_SLOTS 8` already supports it. +4. Add the new `pipeline_id → wov_slot_id` mapping in `test_keyword_new()`. +5. Add a fourth `K_THREAD_STACK_DEFINE` and update `kd_dp_stacks[]`. + +--- + +## Build System Configuration + +### Kconfig (minimum required set) + +```kconfig +# Mandatory +CONFIG_COMP_WOV_ARBITER=y +CONFIG_COMP_KPB=y +CONFIG_COMP_MIXIN_MIXOUT=y +CONFIG_IPC_MAJOR_4=y + +# For the detect_test reference detector +CONFIG_COMP_KWD_DETECT=y + +# For VAD-gated ingress +CONFIG_COMP_VAD_GATE=y # or any other gate component + +# Multi-core scheduling (required for slot 2 on Core 1) +CONFIG_SMP=y +CONFIG_MP_MAX_NUM_CPUS=4 # TGL has 4 DSP cores +CONFIG_SCHED_CPU_MASK_PIN_ONLY=y +``` + +These can be set via `west build -- -DCONFIG_...=y` or by editing the build directory's `zephyr/.config`. + +### Module UUIDs + +| Component | UUID | +|---|---| +| `detect_test` (KWD_TEST) | `1f:d5:a8:eb:27:78:b5:47:82:ee:de:6e:77:43:af:67` | +| `wov_arbiter` | `4a5b6c7d-8e9f-4a1b-2c3d-4e5f60718293` | +| `vad_gate` | `5f:6e:7d:8c:3b:4a:1d:2c:0e:9f:8a:7b:6c:5d:4e:3f` | + +--- + +## Testing and Verification + +### Quick Start (TigerLake / spider DUT) + +```bash +# 0. Load the driver with the WOV firmware and topology +rmmod snd_sof_pci_intel_tgl +modprobe snd_sof_pci_intel_tgl \ + fw_path=intel/sof-ipc4/tgl/community \ + tplg_filename=intel/sof-ipc4-tplg/sof-tgl-dmic-wov-multi.tplg + +# 1. Verify the capture PCM is visible +arecord -l | grep "DMIC Multi-WOV" # expect: card 0: device 11 + +# 2. Run a per-slot trigger test (see below) +``` + +--- + +### Slot Assignment via `wov_init_1NN` kcontrol + +Each `detect_test` instance reads its `wov_slot_id` from a bytes TLV kcontrol +(`wov_init_101 / 102 / 103`, numids 8 / 9 / 10). The assignment must arrive +**before** the PCM `PREPARE` IPC — write the kcontrols immediately after forking +`arecord` into the background: + +```bash +arecord -Dhw:0,11 -r 16000 -c 1 -f S16_LE -d 10 /tmp/wov.wav & +# Race the PREPARE: write all three kcontrols before hw_params completes +python3 set_wov_slot.py 8 0 # wov_init_101 → slot 0 +python3 set_wov_slot.py 9 1 # wov_init_102 → slot 1 +python3 set_wov_slot.py 10 2 # wov_init_103 → slot 2 +wait +``` + +To **disable** a slot (useful for per-slot isolation tests), write +`slot_id = 255` (`WOV_SLOT_INVALID`) — the DP thread is not started for that slot: + +```bash +python3 set_wov_slot.py 8 255 # disable slot 0 +``` + +#### `set_wov_slot.py` — TLV helper script + +Copy to the DUT as `/tmp/set_wov_slot.py`. The script builds the two-level +`snd_ctl_tlv` + `sof_abi_hdr` required by the SOF IPC4 bytes kcontrol path +(`SOF_IPC4_BYTES_CONTROL_PARAM_ID = 202`): + +```python +#!/usr/bin/env python3 +# Usage: set_wov_slot.py +# numid: 8=wov_init_101 (slot 0), 9=wov_init_102 (slot 1), 10=wov_init_103 (slot 2) +# slot_id: 0-2 to enable; 255 to disable (WOV_SLOT_INVALID) +import sys, fcntl, struct, os + +SOF_IPC4_ABI_MAGIC = 0x34464F53 +SOF_CTRL_CMD_BINARY = 3 +SOF_IPC4_BYTES_CONTROL_PARAM_ID = 202 +SNDRV_CTL_IOCTL_TLV_WRITE = (1 << 30) | (8 << 16) | (0x55 << 8) | 0x1b + +numid, slot_id = int(sys.argv[1]), int(sys.argv[2]) + +payload_size = 4 +sizeof_abi_hdr = 32 +inner_length = sizeof_abi_hdr + payload_size # 36 +outer_length = 8 + inner_length # 44 + +abi_hdr = (struct.pack('= KD_DP_FRAMES`). The first batch fires the trigger approximately +14–25 ms after pipeline RUNNING. No physical audio source is required. + +#### Test all three slots simultaneously + +```bash +dmesg -C +arecord -Dhw:0,11 -r 16000 -c 1 -f S16_LE -d 10 /tmp/wov_all.wav & +AREC=$! +python3 set_wov_slot.py 8 0 +python3 set_wov_slot.py 9 1 +python3 set_wov_slot.py 10 2 +wait $AREC +echo "rc=$?" +``` + +Expected: `rc=0`. Slot 0 fires first (timing-dependent); the arbiter pauses slots 1 +and 2 via `NOTIFIER_ID_WOV_CTRL`. + +#### Test a single slot in isolation + +```bash +# Disable the other two slots before starting arecord +python3 set_wov_slot.py 8 255 # disable slot 0 (persists across sessions) +python3 set_wov_slot.py 10 255 # disable slot 2 + +arecord -Dhw:0,11 -r 16000 -c 1 -f S16_LE -d 5 /tmp/wov_s1.wav & +AREC=$! +python3 set_wov_slot.py 9 1 # enable slot 1 only +wait $AREC +echo "rc=$?" +``` + +Repeat for each slot, rotating which `set_wov_slot.py` calls use `255` vs a valid ID. + +#### Verify audio content + +Silence precedes the trigger; real DMIC audio follows. +The silence-to-audio boundary marks the trigger point (∼20 ms into the file). + +```python +import struct, math, sys + +for fname in sys.argv[1:]: + data = open(fname, 'rb').read()[44:] # skip 44-byte WAV header + n = len(data) // 2 + samp = struct.unpack_from(f'<{n}h', data) + nz = next((i for i, x in enumerate(samp) if x != 0), n) + rms = math.sqrt(sum(x*x for x in samp[800:]) / max(len(samp) - 800, 1)) + print(f'{fname}: first_nz={nz*1000/16000:.1f}ms post_trigger_RMS={rms:.0f}') + +# Expected on TGL (silent room): +# first_nz=14-25 ms (arbiter activates after first DP batch) +# post_trigger_RMS > 100 (ambient DMIC noise -- not silence) +``` + +--- + +### Reading Firmware Trace (mtrace) + +The mtrace ring is at `/sys/kernel/debug/sof/mtrace/core{0,1}`. Each `dd` call +advances the FIFO read pointer; start reading **before** the arecord session to +capture pipeline-prepare and trigger messages. + +```bash +# Capture ~1 MB of firmware trace concurrently with an arecord session +dd if=/sys/kernel/debug/sof/mtrace/core0 bs=65536 count=16 of=/tmp/mt.bin & +arecord -Dhw:0,11 -r 16000 -c 1 -f S16_LE -d 5 /tmp/wov.wav & +python3 set_wov_slot.py 8 0 +python3 set_wov_slot.py 9 1 +python3 set_wov_slot.py 10 2 +wait + +# strings works because SOF embeds full format strings in the binary +grep -a 'kd_test\|wov_arb\|AUTO-TRIGGER\|TRIGGERED' /tmp/mt.bin +``` + +For **slot 2** (pinned to Core 1) the DP-thread log is on the Core 1 ring: + +```bash +dd if=/sys/kernel/debug/sof/mtrace/core1 bs=65536 count=4 2>/dev/null | \ + grep -a 'AUTO-TRIGGER' +``` + +--- + +### Expected Trace Events + +After `arecord` opens `hw:0,11` (pipeline prepare + RUNNING): + +``` +kd_test.test_keyword_prepare: comp:4 0x2000d kd_dp thread started for slot 0 +kd_test.test_keyword_prepare: comp:3 0x1000d kd_dp thread started for slot 1 +kd_test.test_keyword_prepare: comp:1 0xd kd_dp thread started for slot 2 (pinned to core 1) +wov_arbiter.wov_arb_trigger: comp:2 0x10 wov_arb_trigger cmd 1 +``` + +On auto-trigger (slot 0 fires first): + +``` +kd_test.default_detect_test_buf: comp:4 0x2000d kd_test dp: AUTO-TRIGGER slot=0 +kd_test.notify_host: comp:4 0x2000d notify_host: WOV module_id=0x2 instance_id=0xd slot_id=0 detected +wov_arbiter.arb_on_detect: comp:2 0x10 wov_arb: activating slot 0 +kd_test.on_wov_ctrl: comp:1 0xd kd: paused (slot 0 active) +kd_test.on_wov_ctrl: comp:3 0x1000d kd: paused (slot 0 active) +kd_test.on_wov_ctrl: comp:4 0x2000d kd: resumed by arbiter +``` + +On stream stop (arecord exits): + +``` +wov_arbiter.wov_arb_trigger: comp:2 0x10 wov_arb_trigger cmd 0 +kd_test.on_wov_ctrl: comp:1 0xd kd: resumed by arbiter +kd_test.on_wov_ctrl: comp:3 0x1000d kd: resumed by arbiter +kd_test.on_wov_ctrl: comp:4 0x2000d kd: resumed by arbiter +``` + +> **Timing note:** if the `PREPARE` IPC arrives before any `wov_init_1NN` kcontrol +> write, the log shows no `kd_dp thread started` for that slot. The slot remains at +> `wov_slot_id = 0xff` and falls back to the 8-second direct-path auto-trigger instead +> of the 20 ms DP-thread path. + +--- + +### Real-Audio Frequency Sweep Test + +Inject tones at known frequencies to trigger specific slots. Run `arecord` on the +DUT and `speaker-test` on a test machine whose audio output is wired to the DUT mic input: + +```bash +# On the DUT: start capture and set all slots +arecord -Dhw:0,11 -r 16000 -c 1 -f S16_LE -d 30 /tmp/wov_sweep.wav & +python3 set_wov_slot.py 8 0 && python3 set_wov_slot.py 9 1 && python3 set_wov_slot.py 10 2 + +# On test machine: drive tone sweep (one frequency band per slot) +speaker-test -c 1 -t sine -f 120 -l 3 # -> slot 0 (Male 80-170 Hz) +speaker-test -c 1 -t sine -f 220 -l 3 # -> slot 1 (Female 175-270 Hz) +speaker-test -c 1 -t sine -f 350 -l 3 # -> slot 2 (Child 275-500 Hz) +wait +``` diff --git a/src/audio/wov_arbiter/wov_arbiter.c b/src/audio/wov_arbiter/wov_arbiter.c new file mode 100644 index 000000000000..590445b3b127 --- /dev/null +++ b/src/audio/wov_arbiter/wov_arbiter.c @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation +// +// 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 the SOF notifier system. +// +// 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 +// +// Notifier events: +// Subscribes to NOTIFIER_ID_WOV_DETECT (detector -> arbiter on keyword) +// Publishes NOTIFIER_ID_WOV_CTRL (arbiter -> detectors: pause/resume) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 { + struct ipc4_base_module_cfg base_cfg; + /* + * Index of the currently active KPB slot (0..num_slots-1). + * WOV_ARB_NO_ACTIVE when no drain is in progress. + * Protected by the scheduler; no extra lock needed. + */ + uint8_t active_slot; + /* Number of input pins (= KPB slots); read from nb_input_pins at init. */ + uint8_t num_slots; +}; + +/* ------------------------------------------------------------------------- + * Notifier callbacks + * ---------------------------------------------------------------------- */ + +/* Notifier callback: a WOV detector has fired. Activates the winning slot + * and broadcasts PAUSE to all detectors via NOTIFIER_ID_WOV_CTRL. */ +static void arb_on_detect(void *arg, enum notify_id id, void *data) +{ + struct comp_dev *dev = arg; + struct wov_arb_data *cd = comp_get_drvdata(dev); + const struct wov_detect_notif *det = data; + + if (det->slot_id >= cd->num_slots) { + comp_err(dev, "wov_arb: bad slot_id %u num_slots=%u", + det->slot_id, cd->num_slots); + 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; + + /* Broadcast PAUSE to all detectors. The winning slot continues draining + * its KPB history buffer; all others suspend detection until RESUME. */ + struct wov_ctrl_notif ctrl = { .cmd = WOV_ARB_CMD_PAUSE, .slot_id = det->slot_id }; + notifier_event(dev, NOTIFIER_ID_WOV_CTRL, NOTIFIER_TARGET_CORE_ALL_MASK, + &ctrl, sizeof(ctrl)); +} + +/* ------------------------------------------------------------------------- + * 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; + } + + const struct ipc4_base_module_cfg *base_cfg = spec; + memcpy_s(&cd->base_cfg, sizeof(cd->base_cfg), base_cfg, sizeof(*base_cfg)); + + /* Read num_slots from nb_input_pins in the extended config when available. + * The old-API spec pointer carries no explicit size, so we validate the + * value and fall back to WOV_ARB_MAX_SLOTS when ext is absent/malformed. */ + const struct ipc4_base_module_cfg_ext *ext = + (const struct ipc4_base_module_cfg_ext *) + ((const uint8_t *)spec + sizeof(*base_cfg)); + if (ext->nb_input_pins > 0 && ext->nb_input_pins <= WOV_ARB_MAX_SLOTS) + cd->num_slots = ext->nb_input_pins; + else + cd->num_slots = WOV_ARB_MAX_SLOTS; + + /* Start with no active slot; first WOV_DETECT notifier will activate one. */ + cd->active_slot = WOV_ARB_NO_ACTIVE; + + comp_set_drvdata(dev, cd); + /* Arbiter produces a capture stream that feeds the host PCM copier. */ + dev->direction = SOF_IPC_STREAM_CAPTURE; + dev->direction_set = true; + dev->state = COMP_STATE_READY; + + comp_info(dev, "wov_arb_new: num_slots=%u", cd->num_slots); + + return dev; +} + +static void wov_arb_free(struct comp_dev *dev) +{ + comp_info(dev, "wov_arb_free"); + + notifier_unregister(dev, NULL, NOTIFIER_ID_WOV_DETECT); + + struct wov_arb_data *cd = comp_get_drvdata(dev); + rfree(cd); + comp_free_device(dev); +} + +static int wov_arb_prepare(struct comp_dev *dev) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + + comp_info(dev, "wov_arb_prepare"); + + cd->active_slot = WOV_ARB_NO_ACTIVE; + + /* Subscribe to keyword-detected events from any WOV detector. */ + notifier_register(dev, NULL, NOTIFIER_ID_WOV_DETECT, arb_on_detect, 0); + + /* Broadcast RESUME so all WOV detector slots start unpaused. */ + struct wov_ctrl_notif ctrl = { .cmd = WOV_ARB_CMD_RESUME }; + notifier_event(dev, NOTIFIER_ID_WOV_CTRL, NOTIFIER_TARGET_CORE_ALL_MASK, + &ctrl, sizeof(ctrl)); + + 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; + + notifier_unregister(dev, NULL, NOTIFIER_ID_WOV_DETECT); + + 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; + + /* + * Stream stopped or paused: deactivate the active slot 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; + struct wov_ctrl_notif c = { .cmd = WOV_ARB_CMD_RESUME }; + notifier_event(dev, NOTIFIER_ID_WOV_CTRL, + NOTIFIER_TARGET_CORE_ALL_MASK, + &c, sizeof(c)); + } + } + + return 0; +} + +static int wov_arb_params(struct comp_dev *dev, + struct sof_ipc_stream_params *params) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + + /* Translate IPC4 base_cfg audio_fmt to IPC3-style stream params. */ + 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; + + return comp_verify_params(dev, 0, params); +} + +/* ------------------------------------------------------------------------- + * IPC4 large-config: allow host to force-select a slot (debug/test use). + * ---------------------------------------------------------------------- */ + +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; +} + +/* ------------------------------------------------------------------------- + * 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. + * 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 >= cd->num_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); + + if (slot == cd->active_slot && copy_bytes > 0) { + uint32_t frame_bytes = audio_stream_frame_bytes(&source->stream); + + /* Round down to whole frames to avoid splitting a sample. */ + if (copy_bytes >= frame_bytes) { + uint32_t aligned = (copy_bytes / frame_bytes) * frame_bytes; + + 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 { + uint32_t avail = audio_stream_get_avail_bytes(&source->stream); + + if (avail > 0) + comp_update_buffer_consume(source, avail); + } + + if (++slot >= cd->num_slots) + break; + } + + if (cd->active_slot == WOV_ARB_NO_ACTIVE && sink_free > 0) { + /* No active drain: push silence so the host copier always has data. + * Split the memset at the circular-buffer wrap point if needed. */ + uint32_t fill_bytes = sink_free; + 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 { + /* Wrap: zero to end of buffer then continue from the start. */ + 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; +} + +/* ------------------------------------------------------------------------- + * IPC4 large-config (get): expose active slot to userspace as a volatile + * RO enum kcontrol; the host reads this via GET_MODULE_LARGE_CONFIG. + * ---------------------------------------------------------------------- */ + +static int wov_arb_get_large_config(struct comp_dev *dev, + uint32_t param_id, + bool first_block, + bool last_block, + uint32_t *data_offset, + char *data) +{ + struct wov_arb_data *cd = comp_get_drvdata(dev); + + if (param_id == IPC4_WOV_ARB_GET_ACTIVE_SLOT) { + *(uint8_t *)data = cd->active_slot; + *data_offset = sizeof(uint8_t); + return 0; + } + + return -EINVAL; +} + +/* ------------------------------------------------------------------------- + * 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, + .set_large_config = wov_arb_set_large_config, + .get_large_config = wov_arb_get_large_config, + .get_attribute = wov_arb_get_attribute, + }, +}; + +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/ipc4/detect_test.h b/src/include/ipc4/detect_test.h index 603bf1741b2c..114289d6c5c9 100644 --- a/src/include/ipc4/detect_test.h +++ b/src/include/ipc4/detect_test.h @@ -30,6 +30,8 @@ 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, + }; + #endif diff --git a/src/include/sof/audio/kpb.h b/src/include/sof/audio/kpb.h index 14c7b33a38e9..bfbd4ab2a332 100644 --- a/src/include/sof/audio/kpb.h +++ b/src/include/sof/audio/kpb.h @@ -21,17 +21,12 @@ #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] */ -#else -/** Due to memory constraints on non-TGL platforms, the buffers are smaller. */ -#define KPB_MAX_BUFF_TIME 2100 /**< time of buffering in miliseconds */ -#define HOST_WAKEUP_TIME 0 /* aprox. time of host DMA wakup from suspend [ms] */ -#endif +#define KPB_MAX_BUFF_TIME CONFIG_KPB_MAX_BUFF_TIME /**< time of buffering in miliseconds */ +#define HOST_WAKEUP_TIME 0 /* host DMA already live; see CONFIG_KPB_MAX_BUFF_TIME */ #define KPB_MAX_DRAINING_REQ (KPB_MAX_BUFF_TIME - HOST_WAKEUP_TIME) #define KPB_MAX_SUPPORTED_CHANNELS 6 /**< number of supported channels */ @@ -42,7 +37,7 @@ struct comp_buffer; #define KPB_MAX_BUFFER_SIZE(sw, channels_number) ((KPB_SAMPLNG_FREQUENCY / 1000) * \ (KPB_SAMPLE_CONTAINER_SIZE(sw) / 8) * KPB_MAX_BUFF_TIME * \ (channels_number)) -#define KPB_MAX_NO_OF_CLIENTS 4 +#define KPB_MAX_NO_OF_CLIENTS CONFIG_KPB_MAX_NO_OF_CLIENTS #define KPB_MAX_SINK_CNT (1 + KPB_MAX_NO_OF_CLIENTS) #define KPB_NO_OF_HISTORY_BUFFERS 2 /**< no of internal buffers */ #define KPB_ALLOCATION_STEP 0x100 diff --git a/src/include/sof/audio/vad_gate.h b/src/include/sof/audio/vad_gate.h new file mode 100644 index 000000000000..f6e3c44598ab --- /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 CONFIG_VAD_GATE_DEFAULT_THRESHOLD +#define VAD_DEFAULT_ONSET_FRAMES CONFIG_VAD_GATE_DEFAULT_ONSET_FRAMES +#define VAD_DEFAULT_HANGOVER CONFIG_VAD_GATE_DEFAULT_HANGOVER_FRAMES +#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/src/include/sof/audio/wov_arbiter.h b/src/include/sof/audio/wov_arbiter.h new file mode 100644 index 000000000000..a1db30a1a3c8 --- /dev/null +++ b/src/include/sof/audio/wov_arbiter.h @@ -0,0 +1,50 @@ +/* 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 + +/* + * IPC4 GET_LARGE_CONFIG param ID: read the currently active slot. + * Host uses this for the volatile RO kcontrol; returns WOV_ARB_NO_ACTIVE + * when no drain is in progress. + */ +#define IPC4_WOV_ARB_GET_ACTIVE_SLOT 2 + +/* Command codes for WOV_CTRL notifier payload (arbiter → detectors). */ +#define WOV_ARB_CMD_PAUSE 0 +#define WOV_ARB_CMD_RESUME 1 + +/* WOV detect notifier payload (detector → arbiter). */ +struct wov_detect_notif { + uint8_t slot_id; /* 0-based detector slot that fired */ +}; + +/* WOV ctrl notifier payload (arbiter → detectors). */ +struct wov_ctrl_notif { + uint8_t cmd; /* WOV_ARB_CMD_PAUSE or WOV_ARB_CMD_RESUME */ + uint8_t slot_id; /* winning slot; receivers skip if this is their own slot */ +}; + +/* Sentinel value matching WOV_SLOT_INVALID in ams_msg.h */ +#define WOV_SLOT_INVALID 0xff + +#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..1978fa06f30a 100644 --- a/src/include/sof/lib/ams_msg.h +++ b/src/include/sof/lib/ams_msg.h @@ -15,4 +15,6 @@ 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 } + + #endif /* __SOF_LIB_AMS_MSG_H__ */ diff --git a/src/include/sof/lib/notifier.h b/src/include/sof/lib/notifier.h index f022b805144c..8b81f559a38f 100644 --- a/src/include/sof/lib/notifier.h +++ b/src/include/sof/lib/notifier.h @@ -31,6 +31,8 @@ enum notify_id { NOTIFIER_ID_DMA_IRQ, /* struct dma_chan_data * */ NOTIFIER_ID_DAI_TRIGGER, /* struct dai_group * */ NOTIFIER_ID_MIC_PRIVACY_STATE_CHANGE, /* struct mic_privacy_settings * */ + NOTIFIER_ID_WOV_DETECT, /* struct wov_detect_notif *: keyword detected */ + NOTIFIER_ID_WOV_CTRL, /* struct wov_ctrl_notif *: pause/resume detectors */ NOTIFIER_ID_COUNT }; diff --git a/src/samples/audio/detect_test.c b/src/samples/audio/detect_test.c index 7979d7db3d6c..2210fe1b3e96 100644 --- a/src/samples/audio/detect_test.c +++ b/src/samples/audio/detect_test.c @@ -28,6 +28,7 @@ #include #if CONFIG_IPC_MAJOR_4 #include +#include #include #endif #include @@ -38,14 +39,10 @@ #include #include #include +#include #include -#if CONFIG_AMS -#include -#include -#include -#else +#include #include -#endif #define ACTIVATION_DEFAULT_SHIFT 3 #define ACTIVATION_DEFAULT_COEF 0.05 @@ -62,10 +59,35 @@ /* default number of samples before detection is activated */ #define KEYPHRASE_DEFAULT_PREAMBLE_LENGTH 0 +/* WOV slot frequency band definitions (fundamental frequency in Hz) */ +#define KD_MALE_FREQ_MIN 80 +#define KD_MALE_FREQ_MAX 170 +#define KD_FEMALE_FREQ_MIN 175 +#define KD_FEMALE_FREQ_MAX 270 +#define KD_CHILD_FREQ_MIN 275 +#define KD_CHILD_FREQ_MAX 500 + +#define NOTIFICATION_DEFAULT_WORD_ID 1 +#define NOTIFICATION_DEFAULT_SCORE 0 + #define KWD_NN_BUFF_ALIGN 64 -static const struct comp_driver comp_keyword; +/* 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 +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; LOG_MODULE_REGISTER(kd_test, CONFIG_SOF_LOG_LEVEL); SOF_DEFINE_REG_UUID(keyword); @@ -90,6 +112,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; @@ -101,11 +129,23 @@ struct comp_data { const struct audio_stream *source, uint32_t frames); struct sof_ipc_comp_event event; -#if CONFIG_AMS - uint32_t kpd_uuid_id; -#else + /* WOV arbiter integration. + * wov_slot_id: slot index (0-2) from topology init_data. + * WOV_SLOT_INVALID means arbiter is not used. + * paused: set by WOV_CTRL PAUSE notifier; cleared on RESUME. + */ + uint8_t wov_slot_id; + bool paused; + + /* DP thread: double-buffered 20ms batch */ + int16_t dp_buf[2][KD_DP_FRAMES]; + uint32_t dp_buf_frames; + uint8_t dp_write_slot; + uint8_t dp_read_slot; + struct k_sem dp_sem; + bool dp_thread_active; + struct kpb_event_data event_data; -#endif /* CONFIG_AMS */ }; static inline bool detector_is_sample_width_supported(enum sof_ipc_frame sf) @@ -139,40 +179,34 @@ static void notify_host(const struct comp_dev *dev) { struct comp_data *cd = comp_get_drvdata(dev); - comp_info(dev, "entry"); + comp_info(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); -#else - ipc_msg_send(cd->msg, &cd->event, true); -#endif /* CONFIG_IPC_MAJOR_4 */ -} - -#if CONFIG_AMS + struct ipc4_voice_cmd_notification notif; + memset_s(¬if, sizeof(notif), 0, sizeof(notif)); -/* Key-phrase detected message*/ -static const ams_uuid_t ams_kpd_msg_uuid = AMS_KPD_MSG_UUID; + 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; -static int ams_notify_kpb(const struct comp_dev *dev) -{ - struct comp_data *cd = comp_get_drvdata(dev); - struct ams_message_payload ams_payload; + /* 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); - cd->client_data.r_ptr = NULL; - cd->client_data.sink = NULL; - cd->client_data.id = 0; /**< TODO: acquire proper id from kpb */ - /* time in milliseconds */ - cd->client_data.drain_req = (cd->drain_req != 0) ? - cd->drain_req : - cd->config.drain_req; + if (cd->msg) + ipc_msg_free(cd->msg); + cd->msg = ipc_msg_w_ext_init(notif.primary.dat, notif.extension.dat, 0); - ams_helper_prepare_payload(dev, &ams_payload, cd->kpd_uuid_id, - (uint8_t *)&cd->client_data, - sizeof(struct kpb_client)); - - return ams_send(&ams_payload); -} + if (cd->msg) + ipc_msg_send(cd->msg, NULL, true); #else + ipc_msg_send(cd->msg, &cd->event, true); +#endif /* CONFIG_IPC_MAJOR_4 */ +} + static void notify_kpb(const struct comp_dev *dev) { struct comp_data *cd = comp_get_drvdata(dev); @@ -181,11 +215,10 @@ static void notify_kpb(const struct comp_dev *dev) cd->client_data.r_ptr = NULL; cd->client_data.sink = NULL; - cd->client_data.id = 0; /**< TODO: acquire proper id from kpb */ - /* time in milliseconds */ + cd->client_data.id = 0; cd->client_data.drain_req = (cd->drain_req != 0) ? - cd->drain_req : - cd->config.drain_req; + cd->drain_req : + cd->config.drain_req; cd->event_data.event_id = KPB_EVENT_BEGIN_DRAINING; cd->event_data.client_data = &cd->client_data; @@ -193,16 +226,156 @@ static void notify_kpb(const struct comp_dev *dev) NOTIFIER_TARGET_CORE_ALL_MASK, &cd->event_data, sizeof(cd->event_data)); } -#endif /* CONFIG_AMS */ + +/* Notifier callback: arbiter is broadcasting a PAUSE or RESUME command. */ +static void on_wov_ctrl(void *arg, enum notify_id id, void *data) +{ + struct comp_dev *dev = arg; + struct comp_data *cd = comp_get_drvdata(dev); + const struct wov_ctrl_notif *n = data; + + if (n->cmd == WOV_ARB_CMD_PAUSE && n->slot_id != cd->wov_slot_id) { + comp_info(dev, "kd: paused (slot %u active)", n->slot_id); + cd->paused = true; + } else { + comp_info(dev, "kd: resumed by arbiter"); + cd->paused = false; + cd->detected = 0; + cd->activation = 0; + cd->detect_preamble = 0; + } +} void detect_test_notify(const struct comp_dev *dev) { + struct comp_data *cd = comp_get_drvdata(dev); + notify_host(dev); -#if CONFIG_AMS - ams_notify_kpb(dev); -#else notify_kpb(dev); -#endif + /* Notify the WOV arbiter which slot fired via SOF notifier. */ + if (cd->wov_slot_id != WOV_SLOT_INVALID) { + struct wov_detect_notif payload = { .slot_id = cd->wov_slot_id }; + + notifier_event(dev, NOTIFIER_ID_WOV_DETECT, + NOTIFIER_TARGET_CORE_ALL_MASK, + &payload, sizeof(payload)); + } +} + +/* 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 != WOV_SLOT_INVALID) ? cd->wov_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 >= KD_MALE_FREQ_MIN && freq_hz <= KD_MALE_FREQ_MAX); + voice_type = "MALE"; + break; + case 1: + freq_match = (freq_hz >= KD_FEMALE_FREQ_MIN && freq_hz <= KD_FEMALE_FREQ_MAX); + voice_type = "FEMALE"; + break; + case 2: + freq_match = (freq_hz >= KD_CHILD_FREQ_MIN && freq_hz <= KD_CHILD_FREQ_MAX); + 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_warn(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; + /* Auto-trigger after ~2s (320 20ms batches) for bench testing. */ + if (cd->frames_total >= 320) { + comp_warn(dev, "kd_test dp: AUTO-TRIGGER slot=%u", + (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 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); + } } static void default_detect_test(struct comp_dev *dev, @@ -218,6 +391,11 @@ 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 != WOV_SLOT_INVALID) ? cd->wov_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 +406,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 +431,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: + freq_match = (freq_hz >= KD_MALE_FREQ_MIN && freq_hz <= KD_MALE_FREQ_MAX); + voice_type = "MALE"; + break; + case 1: + freq_match = (freq_hz >= KD_FEMALE_FREQ_MIN && freq_hz <= KD_FEMALE_FREQ_MAX); + voice_type = "FEMALE"; + break; + case 2: + freq_match = (freq_hz >= KD_CHILD_FREQ_MIN && freq_hz <= KD_CHILD_FREQ_MAX); + 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 (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_warn(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; } - } else { - ++cd->detect_preamble; + } + } + /* 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_warn(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 +641,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); + case SOF_IPC4_BYTES_CONTROL_PARAM_ID: { + /* Topology bytes kcontrol: first byte encodes the WOV slot index. */ + const struct sof_ipc4_control_msg_payload *cp = + (const struct sof_ipc4_control_msg_payload *)data; + if (cp->num_elems >= 1) + cd->wov_slot_id = cp->data[0]; + return 0; + } default: return -EINVAL; } @@ -755,7 +987,14 @@ static struct comp_dev *test_keyword_new(const struct comp_driver *drv, dev->direction_set = true; dev->state = COMP_STATE_READY; + comp_dbg(dev, "test_keyword_new: dev_id=0x%x pipeline_id=%u", + dev_comp_id(dev), dev->ipc_config.pipeline_id); + #if CONFIG_IPC_MAJOR_4 + /* Slot_id is set via the topology bytes kcontrol (set_large_config). + * Default to WOV_SLOT_INVALID until the kernel writes the control. */ + cd->wov_slot_id = WOV_SLOT_INVALID; + struct sof_ipc_stream_params params; /* retrieve params based on base config for IPC4 */ @@ -780,14 +1019,15 @@ static void test_keyword_free(struct comp_dev *dev) comp_info(dev, "entry"); -#if CONFIG_AMS - int ret; + if (cd->wov_slot_id != WOV_SLOT_INVALID) + notifier_unregister(dev, NULL, NOTIFIER_ID_WOV_CTRL); - /* Unregister KD as AMS producer */ - ret = ams_helper_unregister_producer(dev, cd->kpd_uuid_id); - if (ret) - comp_err(dev, "unregister ams error %d", ret); -#endif + /* 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); + } ipc_msg_free(cd->msg); comp_data_blob_handler_free(cd->model_handler); @@ -872,9 +1112,8 @@ static int test_keyword_params(struct comp_dev *dev, } } -#if CONFIG_AMS - cd->kpd_uuid_id = AMS_INVALID_MSG_TYPE; -#endif /* CONFIG_AMS */ + /* Reset paused flag on each params() call (called at prepare time). */ + cd->paused = false; return 0; } @@ -895,6 +1134,7 @@ static int test_keyword_trigger(struct comp_dev *dev, int cmd) cd->detect_preamble = 0; cd->detected = 0; cd->activation = 0; + cd->paused = false; } return 0; @@ -904,10 +1144,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 +1156,43 @@ 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)); - cd->detect_func(dev, &source->stream, frames); + 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); + } + +/* Feed the DP thread when active (WOV mode), otherwise call detect_func + * directly (standalone benchmark mode). */ + 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 if (!cd->dp_thread_active) { + cd->detect_func(dev, &source->stream, frames); + } /* 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 +1206,15 @@ 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; + cd->dp_buf_frames = 0; + cd->dp_write_slot = 0; + 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); + } return comp_set_state(dev, COMP_TRIGGER_RESET); } @@ -985,13 +1264,29 @@ static int test_keyword_prepare(struct comp_dev *dev) &cd->data_blob_size, &cd->data_blob_crc); -#if CONFIG_AMS - /* Register KD as AMS producer */ - ret = ams_helper_register_producer(dev, &cd->kpd_uuid_id, - ams_kpd_msg_uuid); - if (ret) - return ret; -#endif + /* Subscribe to WOV arbiter control events (PAUSE/RESUME). */ + if (cd->wov_slot_id != WOV_SLOT_INVALID) + notifier_register(dev, NULL, NOTIFIER_ID_WOV_CTRL, on_wov_ctrl, 0); + + /* 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); + } + } 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" 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