From cc08d1b0bd6e788cb5c67d62725501b4446d869f Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 11 Oct 2026 13:45:20 +0100 Subject: [PATCH 1/7] audio: component: add comp_driver_find API Add a standard driver lookup API comp_driver_find() to search registered component drivers by UUID (for audio processing modules) or by driver type (for Host and DAI endpoints). Signed-off-by: Liam Girdwood --- src/audio/component.c | 23 +++++++++++++++++++++++ src/include/sof/audio/component.h | 13 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/audio/component.c b/src/audio/component.c index f798e0a971e0..ff68192018ea 100644 --- a/src/audio/component.c +++ b/src/audio/component.c @@ -100,6 +100,29 @@ int comp_set_adapter_ops(const struct comp_driver *drv, const struct module_inte return -ENODEV; } +const struct comp_driver *comp_driver_find(const struct sof_uuid *uuid, uint32_t type) +{ + struct comp_driver_list *drivers = comp_drivers_get(); + struct list_item *clist; + + if (!drivers) + return NULL; + + list_for_item(clist, &drivers->list) { + struct comp_driver_info *info = container_of(clist, struct comp_driver_info, list); + + /* Match by UUID if provided (used for audio processing modules) */ + if (uuid && info->drv->uid && !memcmp(info->drv->uid, uuid, UUID_SIZE)) + return info->drv; + + /* Match by driver type if no UUID is specified (used for Host and DAI endpoints) */ + if (!uuid && info->drv->type == type) + return info->drv; + } + + return NULL; +} + /* NOTE: Keep the component state diagram up to date: * sof-docs/developer_guides/firmware/components/images/comp-dev-states.pu */ diff --git a/src/include/sof/audio/component.h b/src/include/sof/audio/component.h index 48d0764f26d8..69c47d1c54c7 100644 --- a/src/include/sof/audio/component.h +++ b/src/include/sof/audio/component.h @@ -1021,6 +1021,19 @@ void comp_unregister(struct comp_driver_info *drv); */ int comp_set_adapter_ops(const struct comp_driver *drv, const struct module_interface *ops); +/** + * Look up a registered SOF component driver by UUID or driver type. + * + * Scans the global SOF component driver registry to match either a specific + * 128-bit UUID (for audio processing modules) or a generic driver type (for host + * or DAI endpoints). + * + * @param uuid Component UUID to match, or NULL to match by type. + * @param type SOF component driver type (SOF_COMP_HOST, SOF_COMP_DAI, etc.). + * @return Pointer to matched comp_driver, or NULL if not registered. + */ +const struct comp_driver *comp_driver_find(const struct sof_uuid *uuid, uint32_t type); + /** @}*/ /** From be2edde7f748c9967a4b498ca0004a49225f8156 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 11 Oct 2026 13:45:23 +0100 Subject: [PATCH 2/7] audio: pipeline: introduce declarative static topology definitions and headers Add providing declarative topology descriptors, capability bitmasks (rates and frame formats), component constructor macros, ops-driven static module interface, and public control and trigger APIs. Signed-off-by: Liam Girdwood --- .../sof/audio/pipeline/static_pipeline.h | 887 ++++++++++++++++++ 1 file changed, 887 insertions(+) create mode 100644 src/include/sof/audio/pipeline/static_pipeline.h diff --git a/src/include/sof/audio/pipeline/static_pipeline.h b/src/include/sof/audio/pipeline/static_pipeline.h new file mode 100644 index 000000000000..6c9a36f3c319 --- /dev/null +++ b/src/include/sof/audio/pipeline/static_pipeline.h @@ -0,0 +1,887 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +/** + * \file include/sof/audio/pipeline/static_pipeline.h + * \brief Declarative Static Audio Pipeline Engine API + * \author Liam Girdwood + * + * \defgroup static_pipeline_api Static Audio Pipeline Engine + * @{ + * + * The Static Audio Pipeline subsystem provides compile-time and runtime + * declarative topology construction for hostless, microcontroller, and + * standalone embedded platforms running Sound Open Firmware. + * + * It allows platforms to construct complete SOF processing graphs (pipelines, + * modules, DAIs, intermediate buffers, routes, and kcontrols) directly at boot + * without requiring dynamic IPC topology commands from a host operating system. + */ + +#ifndef __SOF_AUDIO_PIPELINE_STATIC_PIPELINE_H__ +#define __SOF_AUDIO_PIPELINE_STATIC_PIPELINE_H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========================================================================= + * Audio Format & Pipeline Capabilities + * ========================================================================= */ + +/** \brief Supported sample rate bitmasks for static capability negotiation */ +#define SOF_STATIC_RATE_8K SOF_RATE_8000 /**< 8000 Hz */ +#define SOF_STATIC_RATE_11K SOF_RATE_11025 /**< 11025 Hz */ +#define SOF_STATIC_RATE_12K SOF_RATE_12000 /**< 12000 Hz */ +#define SOF_STATIC_RATE_16K SOF_RATE_16000 /**< 16000 Hz */ +#define SOF_STATIC_RATE_22K SOF_RATE_22050 /**< 22050 Hz */ +#define SOF_STATIC_RATE_24K SOF_RATE_24000 /**< 24000 Hz */ +#define SOF_STATIC_RATE_32K SOF_RATE_32000 /**< 32000 Hz */ +#define SOF_STATIC_RATE_44K1 SOF_RATE_44100 /**< 44100 Hz */ +#define SOF_STATIC_RATE_48K SOF_RATE_48000 /**< 48000 Hz */ +#define SOF_STATIC_RATE_64K SOF_RATE_64000 /**< 64000 Hz */ +#define SOF_STATIC_RATE_88K2 SOF_RATE_88200 /**< 88200 Hz */ +#define SOF_STATIC_RATE_96K SOF_RATE_96000 /**< 96000 Hz */ +#define SOF_STATIC_RATE_176K4 SOF_RATE_176400 /**< 176400 Hz */ +#define SOF_STATIC_RATE_192K SOF_RATE_192000 /**< 192000 Hz */ +#define SOF_STATIC_RATE_384K (1 << 14) /**< 384000 Hz */ + +/** \brief Supported PCM frame format bitmasks (matching enum sof_ipc_frame) */ +#define SOF_STATIC_FMT_S16_LE BIT(SOF_IPC_FRAME_S16_LE) +#define SOF_STATIC_FMT_S24_4LE BIT(SOF_IPC_FRAME_S24_4LE) +#define SOF_STATIC_FMT_S32_LE BIT(SOF_IPC_FRAME_S32_LE) +#define SOF_STATIC_FMT_FLOAT BIT(SOF_IPC_FRAME_FLOAT) +#define SOF_STATIC_FMT_S24_3LE BIT(SOF_IPC_FRAME_S24_3LE) +#define SOF_STATIC_FMT_S24_4LE_MSB BIT(SOF_IPC_FRAME_S24_4LE_MSB) +#define SOF_STATIC_FMT_U8 BIT(SOF_IPC_FRAME_U8) +#define SOF_STATIC_FMT_S16_4LE BIT(SOF_IPC_FRAME_S16_4LE) +#define SOF_STATIC_FMT_A_LAW BIT(SOF_IPC_FRAME_A_LAW) +#define SOF_STATIC_FMT_MU_LAW BIT(SOF_IPC_FRAME_MU_LAW) + +/** Mask covering all supported sample rates */ +#define SOF_STATIC_RATE_MASK_ALL \ + (SOF_STATIC_RATE_8K | SOF_STATIC_RATE_11K | SOF_STATIC_RATE_12K | \ + SOF_STATIC_RATE_16K | SOF_STATIC_RATE_22K | SOF_STATIC_RATE_24K | \ + SOF_STATIC_RATE_32K | SOF_STATIC_RATE_44K1 | SOF_STATIC_RATE_48K | \ + SOF_STATIC_RATE_64K | SOF_STATIC_RATE_88K2 | SOF_STATIC_RATE_96K | \ + SOF_STATIC_RATE_176K4 | SOF_STATIC_RATE_192K | SOF_STATIC_RATE_384K) + +/** Mask covering all supported frame formats */ +#define SOF_STATIC_FMT_MASK_ALL \ + (SOF_STATIC_FMT_S16_LE | SOF_STATIC_FMT_S24_4LE | SOF_STATIC_FMT_S32_LE | \ + SOF_STATIC_FMT_FLOAT | SOF_STATIC_FMT_S24_3LE | SOF_STATIC_FMT_S24_4LE_MSB | \ + SOF_STATIC_FMT_U8 | SOF_STATIC_FMT_S16_4LE | SOF_STATIC_FMT_A_LAW | \ + SOF_STATIC_FMT_MU_LAW) + +/** + * \brief Audio stream format and sample rate capability descriptor. + */ +struct sof_static_caps { + uint32_t formats; /**< Bitmask of supported frame formats (SOF_STATIC_FMT_*) */ + uint32_t rates; /**< Bitmask of supported sample rates (SOF_STATIC_RATE_*) */ + uint16_t min_channels; /**< Minimum channel count */ + uint16_t max_channels; /**< Maximum channel count */ + uint32_t default_rate; /**< Default sample rate in Hz (e.g. 48000) */ + uint32_t sink_rate; /**< Target sink sample rate for converters (SRC/ASRC, 0 = same as default_rate) */ + enum sof_ipc_frame default_fmt; /**< Default frame format */ +}; + +/** + * \brief Helper macro to initialize a static capability structure. + * \param _fmt Frame format (e.g. SOF_IPC_FRAME_S16_LE or SOF_IPC_FRAME_FLOAT). + * \param _rate Default sample rate in Hz (e.g. 48000). + * \param _ch Number of audio channels (e.g. 2). + */ +#define SOF_STATIC_CAPS(_fmt, _rate, _ch) \ + { \ + .formats = BIT(_fmt), \ + .rates = (_rate == 8000) ? SOF_STATIC_RATE_8K : \ + (_rate == 11025) ? SOF_STATIC_RATE_11K : \ + (_rate == 12000) ? SOF_STATIC_RATE_12K : \ + (_rate == 16000) ? SOF_STATIC_RATE_16K : \ + (_rate == 22050) ? SOF_STATIC_RATE_22K : \ + (_rate == 24000) ? SOF_STATIC_RATE_24K : \ + (_rate == 32000) ? SOF_STATIC_RATE_32K : \ + (_rate == 44100) ? SOF_STATIC_RATE_44K1 : \ + (_rate == 48000) ? SOF_STATIC_RATE_48K : \ + (_rate == 64000) ? SOF_STATIC_RATE_64K : \ + (_rate == 88200) ? SOF_STATIC_RATE_88K2 : \ + (_rate == 96000) ? SOF_STATIC_RATE_96K : \ + (_rate == 176400) ? SOF_STATIC_RATE_176K4 : \ + (_rate == 192000) ? SOF_STATIC_RATE_192K : \ + (_rate == 384000) ? SOF_STATIC_RATE_384K : SOF_STATIC_RATE_48K, \ + .min_channels = (_ch), \ + .max_channels = (_ch), \ + .default_rate = (_rate), \ + .default_fmt = (_fmt), \ + } + +/* ========================================================================= + * Components / Processing Modules + * ========================================================================= */ + +/** + * \brief Category of a static pipeline component. + */ +enum sof_static_comp_type { + SOF_STATIC_COMP_MODULE = 0, /**< Audio processing module (Volume, EQ, DRC, etc.) */ + SOF_STATIC_COMP_HOST, /**< Host or USB streaming endpoint */ + SOF_STATIC_COMP_DAI, /**< Hardware Digital Audio Interface (I2S, PDM, SAI, SPDIF) */ +}; + +/** + * \brief Hardware endpoint binding type. + */ +enum sof_static_ep_type { + SOF_STATIC_EP_NONE = 0, /**< Standard internal processing module */ + SOF_STATIC_EP_USB_TERMINAL, /**< USB Audio Class 2 (UAC2) streaming terminal */ + SOF_STATIC_EP_DAI, /**< Physical hardware DAI controller */ +}; + +/** + * \brief Declarative component descriptor. + */ +struct sof_static_comp { + uint32_t id; /**< Unique component identifier */ + uint32_t pipeline_id; /**< Owning pipeline identifier */ + const char *name; /**< Human-readable diagnostic name (e.g. "VOL_PB") */ + enum sof_static_comp_type type; /**< Component category: Module, Host, or DAI */ + const struct sof_uuid *uuid; /**< Registered driver UUID (NULL for standard Host or DAI) */ + uint32_t direction; /**< Audio stream direction: SOF_IPC_STREAM_PLAYBACK or CAPTURE */ + struct sof_static_caps caps; /**< Format and sample rate capabilities */ + + enum sof_static_ep_type ep_type; /**< Endpoint binding type */ + union { + struct { + uint32_t terminal_id; /**< Bound UAC2 USB Terminal Entity ID */ + } usb; + struct { + uint32_t dai_type; /**< DAI type (e.g. SOF_DAI_ESP32_I2S, SOF_DAI_IMX_SAI) */ + uint32_t dai_index; /**< DAI hardware instance index */ + uint32_t format; /**< Protocol/clock format mask (SOF_DAI_FMT_*) */ + int (*configure)(struct comp_dev *dev, + struct ipc_config_dai *dai_cfg, + void *spec_cfg); /**< Optional platform DAI endpoint config hook */ + } dai; + } ep; + + const void *init_blob; /**< Optional initial ABI configuration blob */ + size_t init_blob_size; /**< Size of initial configuration blob in bytes */ +}; + +/** + * \brief Declare a processing module component. + */ +#define SOF_STATIC_COMP_MODULE(...) \ + { .type = SOF_STATIC_COMP_MODULE, __VA_ARGS__ } + +/** + * \brief Declare a USB/Host streaming endpoint component. + */ +#define SOF_STATIC_COMP_HOST(...) \ + { .type = SOF_STATIC_COMP_HOST, .ep_type = SOF_STATIC_EP_USB_TERMINAL, __VA_ARGS__ } + +/** + * \brief Declare a hardware DAI endpoint component. + */ +#define SOF_STATIC_COMP_DAI(...) \ + { .type = SOF_STATIC_COMP_DAI, .ep_type = SOF_STATIC_EP_DAI, __VA_ARGS__ } + +/* ========================================================================= + * Module Constructor Macros + * ========================================================================= */ + +/** + * \brief Generic constructor macro for an audio processing module component. + * + * \param _id Unique component ID. + * \param _ppl Owning pipeline ID. + * \param _name Human-readable component name string. + * \param _dir Stream direction (SOF_IPC_STREAM_PLAYBACK or SOF_IPC_STREAM_CAPTURE). + * \param _uuid Pointer to component driver UUID (e.g. &volume_uuid). + * \param _fmt Default PCM frame format (enum sof_ipc_frame). + * \param _rate Default stream sample rate in Hz. + * \param _ch Default channel count. + * \param _blob Optional pointer to initial configuration blob (or NULL). + * \param _blob_sz Size of initial configuration blob in bytes (or 0). + */ +#define SOF_STATIC_MODULE(_id, _ppl, _name, _dir, _uuid, _fmt, _rate, _ch, _blob, _blob_sz) \ + { \ + .id = (_id), \ + .pipeline_id = (_ppl), \ + .name = (_name), \ + .type = SOF_STATIC_COMP_MODULE, \ + .uuid = (_uuid), \ + .direction = (_dir), \ + .caps = SOF_STATIC_CAPS((_fmt), (_rate), (_ch)), \ + .init_blob = (_blob), \ + .init_blob_size = (_blob_sz), \ + } + +/** + * \brief Generic constructor macro for a sample rate converter component (SRC / ASRC). + * + * \param _id Unique component ID. + * \param _ppl Owning pipeline ID. + * \param _name Human-readable component name string. + * \param _dir Stream direction (SOF_IPC_STREAM_PLAYBACK or SOF_IPC_STREAM_CAPTURE). + * \param _uuid Pointer to component driver UUID (e.g. &src_uuid, &asrc_uuid). + * \param _fmt Default PCM frame format (enum sof_ipc_frame). + * \param _in_rate Source/input sample rate in Hz. + * \param _out_rate Sink/output sample rate in Hz. + * \param _ch Channel count. + */ +#define SOF_STATIC_MODULE_RATE_CONV(_id, _ppl, _name, _dir, _uuid, _fmt, _in_rate, _out_rate, _ch) \ + { \ + .id = (_id), \ + .pipeline_id = (_ppl), \ + .name = (_name), \ + .type = SOF_STATIC_COMP_MODULE, \ + .uuid = (_uuid), \ + .direction = (_dir), \ + .caps = { \ + .formats = BIT(_fmt), \ + .rates = SOF_STATIC_RATE_MASK_ALL, \ + .min_channels = (_ch), \ + .max_channels = (_ch), \ + .default_rate = (_in_rate), \ + .sink_rate = (_out_rate), \ + .default_fmt = (_fmt), \ + }, \ + } + +/* ========================================================================= + * Endpoint Constructor Macros + * ========================================================================= */ + +/** + * \brief Simple constructor macro for a USB / UAC2 streaming endpoint. + * + * \param _id Unique component ID. + * \param _ppl Owning pipeline ID. + * \param _name Human-readable component name. + * \param _dir Stream direction (SOF_IPC_STREAM_PLAYBACK or SOF_IPC_STREAM_CAPTURE). + * \param _fmt Default PCM frame format (enum sof_ipc_frame). + * \param _rate Stream sample rate in Hz. + * \param _ch Channel count. + * \param _term_id USB Audio Class Terminal ID. + */ +#define SOF_STATIC_ENDPOINT_USB(_id, _ppl, _name, _dir, _fmt, _rate, _ch, _term_id) \ + { \ + .id = (_id), \ + .pipeline_id = (_ppl), \ + .name = (_name), \ + .type = SOF_STATIC_COMP_HOST, \ + .direction = (_dir), \ + .caps = SOF_STATIC_CAPS((_fmt), (_rate), (_ch)), \ + .ep_type = SOF_STATIC_EP_USB_TERMINAL, \ + .ep.usb.terminal_id = (_term_id), \ + } + +/** + * \brief Generic constructor macro for a hardware Digital Audio Interface (DAI) endpoint. + * + * \param _id Unique component ID. + * \param _ppl Owning pipeline ID. + * \param _name Human-readable component name. + * \param _dir Stream direction (SOF_IPC_STREAM_PLAYBACK or SOF_IPC_STREAM_CAPTURE). + * \param _fmt Default PCM frame format (enum sof_ipc_frame). + * \param _rate Stream sample rate in Hz. + * \param _ch Channel count. + * \param _type Hardware DAI type (e.g. SOF_DAI_INTEL_SSP, SOF_DAI_ESP32_I2S, etc.). + * \param _idx Hardware DAI controller instance index. + * \param _clk_fmt DAI clock format flags (e.g. SOF_DAI_FMT_I2S | SOF_DAI_FMT_CBC_CFC). + */ +#define SOF_STATIC_ENDPOINT_DAI(_id, _ppl, _name, _dir, _fmt, _rate, _ch, _type, _idx, _clk_fmt) \ + { \ + .id = (_id), \ + .pipeline_id = (_ppl), \ + .name = (_name), \ + .type = SOF_STATIC_COMP_DAI, \ + .direction = (_dir), \ + .caps = SOF_STATIC_CAPS((_fmt), (_rate), (_ch)), \ + .ep_type = SOF_STATIC_EP_DAI, \ + .ep.dai.dai_type = (_type), \ + .ep.dai.dai_index = (_idx), \ + .ep.dai.format = (_clk_fmt), \ + } + +/** + * \brief Constructor macro for a hardware DAI endpoint with custom platform configuration callback. + * + * \param _id Unique component ID. + * \param _ppl Owning pipeline ID. + * \param _name Human-readable component name. + * \param _dir Stream direction (SOF_IPC_STREAM_PLAYBACK or SOF_IPC_STREAM_CAPTURE). + * \param _fmt Default PCM frame format (enum sof_ipc_frame). + * \param _rate Stream sample rate in Hz. + * \param _ch Channel count. + * \param _type Hardware DAI type (e.g. SOF_DAI_INTEL_SSP, SOF_DAI_ESP32_I2S, etc.). + * \param _idx Hardware DAI controller instance index. + * \param _clk_fmt DAI clock format flags (e.g. SOF_DAI_FMT_I2S | SOF_DAI_FMT_CBC_CFC). + * \param _cfg_fn Endpoint configuration callback function pointer. + */ +#define SOF_STATIC_ENDPOINT_DAI_CFG(_id, _ppl, _name, _dir, _fmt, _rate, _ch, _type, _idx, _clk_fmt, _cfg_fn) \ + { \ + .id = (_id), \ + .pipeline_id = (_ppl), \ + .name = (_name), \ + .type = SOF_STATIC_COMP_DAI, \ + .direction = (_dir), \ + .caps = SOF_STATIC_CAPS((_fmt), (_rate), (_ch)), \ + .ep_type = SOF_STATIC_EP_DAI, \ + .ep.dai.dai_type = (_type), \ + .ep.dai.dai_index = (_idx), \ + .ep.dai.format = (_clk_fmt), \ + .ep.dai.configure = (_cfg_fn), \ + } + +/* ========================================================================= + * Static Module Operations + * ========================================================================= */ + +/** + * \brief Static pipeline module driver operations. + * + * Audio processing modules register these operations to handle static + * instantiation (synthesizing any module-specific IPC init configuration) + * and runtime kcontrol manipulation (volume, switch/mute, enum) without + * requiring the generic pipeline loader to contain module-specific logic. + */ +struct sof_static_module_ops { + /** UUID of target audio component */ + const struct sof_uuid *uuid; + + /** + * \brief Instantiate and initialize the module component. + * \param[in] drv SOF component driver. + * \param[in] cfg IPC component configuration. + * \param[in] cdesc Static component descriptor. + * \param[in] period_us Owning pipeline scheduling period in microseconds. + * \return Created component device pointer, or NULL on error. + */ + struct comp_dev *(*create)(const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us); + + /** + * \brief Apply linear volume gain to component. + * \param[in] dev Component device pointer. + * \param[in] channels Channel count to apply volume across. + * \param[in] val Linear volume value (0 to INT32_MAX). + * \return 0 on success, negative error code on failure. + */ + int (*apply_volume)(struct comp_dev *dev, uint32_t channels, int32_t val); + + /** + * \brief Apply linear volume gain to a specific channel. + * \param[in] dev Component device pointer. + * \param[in] channel Channel index (0-based). + * \param[in] val Linear volume value (0 to INT32_MAX). + * \return 0 on success, negative error code on failure. + */ + int (*apply_volume_chan)(struct comp_dev *dev, uint32_t channel, int32_t val); + + /** + * \brief Apply binary switch/mute state to component. + * \param[in] dev Component device pointer. + * \param[in] channels Channel count. + * \param[in] val Binary switch value (0 = muted/bypass, 1 = unmuted/active). + * \return 0 on success, negative error code on failure. + */ + int (*apply_switch)(struct comp_dev *dev, uint32_t channels, int32_t val); + + /** + * \brief Apply binary switch/mute state to a specific channel. + * \param[in] dev Component device pointer. + * \param[in] channel Channel index (0-based). + * \param[in] val Binary switch value (0 = muted/bypass, 1 = unmuted/active). + * \return 0 on success, negative error code on failure. + */ + int (*apply_switch_chan)(struct comp_dev *dev, uint32_t channel, int32_t val); + + /** + * \brief Apply enumerated route/value to component. + * \param[in] dev Component device pointer. + * \param[in] channel Channel index. + * \param[in] val Selected enumeration value. + * \return 0 on success, negative error code on failure. + */ + int (*apply_enum)(struct comp_dev *dev, uint32_t channel, int32_t val); + + /** + * \brief Apply raw binary configuration or coefficient blob to component. + * \param[in] dev Component device pointer. + * \param[in] data Pointer to binary data blob. + * \param[in] size Size of binary data in bytes. + * \return 0 on success, negative error code on failure. + */ + int (*apply_binary)(struct comp_dev *dev, const void *data, size_t size); + + /** List node for registered ops */ + struct list_item list; +}; + +/** + * \brief Macro to automatically register static module operations at system startup. + * + * In Zephyr RTOS environments, registers using SYS_INIT at APPLICATION level. + * In non-Zephyr environments (e.g. POSIX tests), registers using constructor attribute. + * + * \param name Unique identifier suffix for initialization routine. + * \param ops Pointer to struct sof_static_module_ops. + */ +#if defined(__ZEPHYR__) +#include +#define DECLARE_STATIC_MODULE_OPS(name, ops) \ + static int _static_ops_init_##name(void) \ + { \ + return sof_static_register_module_ops(ops); \ + } \ + SYS_INIT(_static_ops_init_##name, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY) +#else +#define DECLARE_STATIC_MODULE_OPS(name, ops) \ + static __attribute__((constructor)) void _static_ops_init_##name(void) \ + { \ + sof_static_register_module_ops(ops); \ + } +#endif + +/* ========================================================================= + * Intermediate Audio Buffers & Pipeline Routing + * ========================================================================= */ + +/** + * \brief Declarative intermediate audio buffer descriptor. + */ +struct sof_static_buffer { + uint32_t id; /**< Unique buffer identifier */ + size_t size; /**< Total buffer allocation size in bytes */ + enum sof_ipc_frame fmt; /**< Buffer audio frame format (SOF_IPC_FRAME_INVALID = inherit from producer) */ + uint32_t rate; /**< Sample rate in Hz (0 = inherit from producer component) */ + uint16_t channels; /**< Channel count (0 = inherit from producer component) */ + uint32_t flags; /**< Memory allocation flags (e.g. SOF_MEM_FLAG_DMA) */ +}; + +/** + * \brief Helper macro to declare an intermediate audio buffer with standard flags. + * + * Initializes memory allocation flags to standard DMA and user memory, and defaults + * the format to SOF_IPC_FRAME_INVALID to inherit the PCM format from the upstream producer. + */ +#define SOF_STATIC_BUFFER(...) \ + { .flags = SOF_MEM_FLAG_DMA | SOF_MEM_FLAG_USER, .fmt = SOF_IPC_FRAME_INVALID, __VA_ARGS__ } + +/** + * \brief Declarative graph route connecting an upstream component to a downstream component. + */ +struct sof_static_route { + uint32_t src_comp_id; /**< Upstream producer component ID */ + uint32_t buffer_id; /**< Intermediate buffer ID linking the components */ + uint32_t sink_comp_id; /**< Downstream consumer component ID */ +}; + +/** + * \brief Helper macro to declare a connection route. + */ +#define SOF_STATIC_ROUTE(...) \ + { __VA_ARGS__ } + +/* ========================================================================= + * PCMs (Pulse Code Modulated Endpoints) + * ========================================================================= */ + +/** + * \brief Declarative PCM endpoint descriptor. + */ +struct sof_static_pcm { + uint32_t pcm_id; /**< Logical PCM stream index */ + const char *name; /**< Human-readable PCM name (e.g. "Speaker Playback") */ + uint32_t direction; /**< Audio direction: SOF_IPC_STREAM_PLAYBACK or CAPTURE */ + uint32_t pipeline_id; /**< Associated pipeline ID */ + uint32_t host_comp_id; /**< Host/USB endpoint component ID */ + struct sof_static_caps caps; /**< Supported PCM capabilities */ +}; + +/** + * \brief Helper macro to declare a static PCM stream. + */ +#define SOF_STATIC_PCM(...) \ + { __VA_ARGS__ } + +/* ========================================================================= + * Kcontrols (Volume, Mute, Bypass, Presets, Coefficients) + * ========================================================================= */ + +/** + * \brief Type of static kcontrol. + */ +enum sof_static_ctrl_type { + SOF_STATIC_CTRL_VOLUME = 0, /**< Linear or dB volume fader */ + SOF_STATIC_CTRL_SWITCH, /**< Boolean mute or module bypass switch */ + SOF_STATIC_CTRL_ENUM, /**< Multi-value enumeration selector */ + SOF_STATIC_CTRL_BINARY, /**< Raw binary parameter or coefficient blob */ +}; + +/** + * \brief Declarative static kcontrol descriptor. + */ +struct sof_static_kcontrol { + uint32_t id; /**< Unique control identifier */ + const char *name; /**< Human-readable control name (e.g. "Master Volume") */ + enum sof_static_ctrl_type type; /**< Control type */ + uint32_t target_comp_id; /**< Target component ID affected by this control (0 for custom) */ + uint32_t param_id; /**< Optional parameter or command index */ + + int32_t min; /**< Minimum valid value */ + int32_t max; /**< Maximum valid value */ + int32_t def; /**< Default initial value applied at boot */ + uint32_t channels; /**< Number of channels affected by this control */ + + uint8_t uac2_entity_id; /**< Bound external entity ID (e.g. UAC2 Feature Unit) */ +}; + +/** \brief Helper macro to declare a volume kcontrol */ +#define SOF_STATIC_KCONTROL_VOLUME(...) \ + { .type = SOF_STATIC_CTRL_VOLUME, __VA_ARGS__ } + +/** \brief Helper macro to declare a boolean switch/bypass kcontrol */ +#define SOF_STATIC_KCONTROL_SWITCH(...) \ + { .type = SOF_STATIC_CTRL_SWITCH, __VA_ARGS__ } + +/** \brief Helper macro to declare an enumeration selector kcontrol */ +#define SOF_STATIC_KCONTROL_ENUM(...) \ + { .type = SOF_STATIC_CTRL_ENUM, __VA_ARGS__ } + +/** \brief Helper macro to declare a binary configuration kcontrol */ +#define SOF_STATIC_KCONTROL_BINARY(...) \ + { .type = SOF_STATIC_CTRL_BINARY, __VA_ARGS__ } + +/* ========================================================================= + * Pipeline Descriptors & Top-Level Topology + * ========================================================================= */ + +/** + * \brief Declarative audio pipeline descriptor. + */ +struct sof_static_pipeline_desc { + uint32_t pipeline_id; /**< Unique pipeline identifier */ + const char *name; /**< Human-readable pipeline name */ + uint32_t direction; /**< Audio direction: SOF_IPC_STREAM_PLAYBACK or CAPTURE */ + uint32_t priority; /**< Scheduling priority (0 = normal) */ + uint32_t core; /**< Core affinity index (0, 1, ...) */ + uint32_t period; /**< Scheduling period in microseconds (e.g. 1000) */ + uint32_t frames_per_sched; /**< Number of frames processed per schedule tick */ + uint32_t time_domain; /**< Scheduling time domain (SOF_TIME_DOMAIN_TIMER or DMA) */ + uint32_t default_rate; /**< Default stream rate in Hz (e.g. 48000, 0 = auto) */ + uint16_t default_channels; /**< Default channel count (e.g. 2, 0 = auto) */ + uint32_t sched_comp_id; /**< Component driving pipeline scheduling */ + uint32_t source_comp_id; /**< Primary source endpoint component ID */ + uint32_t sink_comp_id; /**< Primary sink endpoint component ID */ +}; + +/** + * \brief Custom kcontrol callback handler type. + * + * Allows platforms or applications to register custom kcontrol dispatch + * logic for non-module controls (e.g. clock modes, routes, injector switches). + * + * \param[in] ctl Pointer to the static kcontrol descriptor. + * \param[in] val Value to write. + * \param[in,out] priv Private user/platform context. + * \return 0 on success, negative errno on failure. + */ +typedef int (*sof_static_kcontrol_handler_fn)(const struct sof_static_kcontrol *ctl, + int32_t val, void *priv); + +/** + * \brief Top-level static audio topology descriptor. + */ +struct sof_static_topology { + const char *name; /**< Human-readable topology name */ + size_t num_pipelines; /**< Number of pipelines in this topology */ + const struct sof_static_pipeline_desc *pipelines; /**< Array of pipeline descriptors */ + size_t num_comps; /**< Number of components */ + const struct sof_static_comp *comps; /**< Array of component descriptors */ + size_t num_buffers; /**< Number of intermediate buffers */ + const struct sof_static_buffer *buffers; /**< Array of buffer descriptors */ + size_t num_routes; /**< Number of graph connection routes */ + const struct sof_static_route *routes; /**< Array of connection routes */ + size_t num_pcms; /**< Number of logical PCMs */ + const struct sof_static_pcm *pcms; /**< Array of PCM descriptors */ + size_t num_controls; /**< Number of kcontrols */ + const struct sof_static_kcontrol *controls; /**< Array of kcontrol descriptors */ + + sof_static_kcontrol_handler_fn custom_control_handler; /**< Optional platform control hook */ + void *custom_control_data; /**< Platform context passed to hook */ +}; + +/* ========================================================================= + * Generic Engine Public APIs + * ========================================================================= */ + +/** + * \brief Initialize and instantiate the static topology graph. + * + * Allocates pipelines, instantiates components from the driver registry, + * initializes intermediate buffers, establishes graph connections, negotiates + * component stream parameters, and prepares all pipelines for streaming. + * + * \param[in] topo Pointer to the target static topology definition. + * \return 0 on success, negative errno on failure. + */ +int sof_static_topology_init(const struct sof_static_topology *topo); + +/** + * \brief Retrieve the currently active static topology descriptor. + * \return Pointer to active topology descriptor, or NULL if uninitialized. + */ +const struct sof_static_topology *sof_static_topology_get(void); + +/** + * \brief Retrieve a pipeline instance by its pipeline ID. + * \param[in] pipeline_id Unique pipeline ID. + * \return Pointer to struct pipeline, or NULL if not found. + */ +struct pipeline *sof_static_pipeline_get(uint32_t pipeline_id); + +/** + * \brief Retrieve a component device instance by its component ID. + * \param[in] comp_id Unique component ID. + * \return Pointer to struct comp_dev, or NULL if not found. + */ +struct comp_dev *sof_static_comp_get(uint32_t comp_id); + +/** + * \brief Retrieve an intermediate buffer instance by its buffer ID. + * \param[in] buffer_id Unique buffer ID. + * \return Pointer to struct comp_buffer, or NULL if not found. + */ +struct comp_buffer *sof_static_buffer_get(uint32_t buffer_id); + +/** + * \brief Register static module operations for an audio component. + * + * Audio processing modules register static operations at boot or module load + * time to handle static component instantiation and kcontrol dispatch without + * hardcoded component logic in the generic pipeline engine. + * + * \param[in,out] ops Pointer to module operations structure. + * \return 0 on success, negative error code on failure. + */ +int sof_static_register_module_ops(struct sof_static_module_ops *ops); + +/** + * \brief Find registered static module operations by component UUID. + * \param[in] uuid UUID of target component. + * \return Pointer to registered ops, or NULL if not found. + */ +const struct sof_static_module_ops *sof_static_find_module_ops(const struct sof_uuid *uuid); + +struct ipc4_base_module_cfg; + +/** + * \brief Synthesize standard IPC4 base module configuration structure. + * \param[out] base_cfg Target base configuration to populate. + * \param[in] cdesc Static component descriptor with format and rate capabilities. + * \param[in] period_us Pipeline scheduling period in microseconds. + */ +void sof_static_init_base_cfg(struct ipc4_base_module_cfg *base_cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us); + +/** + * \brief Default fallback creation for standard IPC4 module adapters. + * \param[in] drv SOF component driver. + * \param[in] cfg IPC component configuration. + * \param[in] cdesc Static component descriptor. + * \param[in] period_us Owning pipeline scheduling period in microseconds. + * \return Created comp_dev pointer, or NULL on failure. + */ +struct comp_dev *sof_static_module_create_default(const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us); + +/** + * \brief Set kcontrol value by control ID. + * + * Dispatches to the target module via its registered static operations + * (struct sof_static_module_ops), or invokes the topology's custom_control_handler. + * + * \param[in] ctrl_id Unique control ID. + * \param[in] val Value to apply. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_set(uint32_t ctrl_id, int32_t val); + +/** + * \brief Set kcontrol value for a specific audio channel. + * + * Dispatches control value to a specific audio channel of the target component + * via apply_volume_chan, apply_switch_chan, or apply_enum. + * + * \param[in] ctrl_id Unique control ID. + * \param[in] channel Channel index (0-based). + * \param[in] val Value to apply. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_set_chan(uint32_t ctrl_id, uint32_t channel, int32_t val); + +/** + * \brief Set binary raw configuration blob on a static kcontrol by control ID. + * + * Dispatches the raw binary configuration blob to the target component associated + * with the designated kcontrol via apply_binary or module adapter large config. + * + * \param[in] ctrl_id Target kcontrol identifier. + * \param[in] data Pointer to raw configuration blob. + * \param[in] size Size of data blob in bytes. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_set_binary(uint32_t ctrl_id, const void *data, size_t size); + +/** + * \brief Retrieve cached kcontrol value by control ID. + * \param[in] ctrl_id Unique control ID. + * \param[out] val Pointer to store retrieved control value. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_get(uint32_t ctrl_id, int32_t *val); + +/** + * \brief Find kcontrol ID by human-readable name. + * \param[in] name Control name string to search for. + * \return Control ID >= 0 if found, -ENOENT if not found. + */ +int sof_static_kcontrol_find_by_name(const char *name); + +/** + * \brief Dispatch UAC2 feature unit control to registered kcontrols. + * \param[in] entity_id UAC2 Feature Unit Entity ID. + * \param[in] channel Audio channel index (0 = master/all). + * \param[in] val Value from USB request (e.g. 8.8 fixed-point dB volume or boolean mute). + * \param[in] is_volume True for volume command, false for mute command. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_set_by_uac2(uint8_t entity_id, uint8_t channel, int32_t val, bool is_volume); + +/** + * \brief Retrieve kcontrol value formatted for UAC2 feature unit response. + * \param[in] entity_id UAC2 Feature Unit Entity ID. + * \param[in] channel Audio channel index. + * \param[out] val Pointer to store retrieved value. + * \param[in] is_volume True for volume query, false for mute query. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_get_by_uac2(uint8_t entity_id, uint8_t channel, int32_t *val, bool is_volume); + +/** + * \brief Trigger pipeline start or stop associated with a UAC2 terminal ID. + * \param[in] terminal_id Bound UAC2 Terminal Entity ID. + * \param[in] start True to start pipeline, false to pause/stop. + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_trigger_by_uac2_term(uint8_t terminal_id, bool start); + +/** + * \brief Start a static pipeline by pipeline ID. + * + * Prepares the pipeline and constituent components if needed, propagates + * start triggers, transitions state to COMP_STATE_ACTIVE, and activates + * scheduling copy tasks. + * + * \param[in] pipeline_id Target pipeline ID. + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_start(uint32_t pipeline_id); + +/** + * \brief Stop a static pipeline by pipeline ID. + * + * Propagates stop triggers, cancels scheduling copy tasks, and transitions + * pipeline state to COMP_STATE_PAUSED. + * + * \param[in] pipeline_id Target pipeline ID. + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_stop(uint32_t pipeline_id); + +/** + * \brief Directly trigger a static pipeline start or stop by pipeline ID. + * + * Synchronously transitions the pipeline state machine, propagates triggers + * to all constituent components, and manages scheduling task execution. + * Dispatches directly to sof_static_pipeline_start() or sof_static_pipeline_stop(). + * + * \param[in] pipeline_id Target pipeline ID. + * \param[in] start True to start pipeline, false to stop. + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_trigger(uint32_t pipeline_id, bool start); + +/** + * \brief Re-negotiate the stream sample rate for a specific static pipeline. + * + * Re-negotiates the sample rate for the designated pipeline and its constituent + * components and buffers while the pipeline is in paused or stopped state. + * Validates that all constituent components support the requested rate according + * to their capability masks, reconfigures buffer period sizes, and propagates + * pipeline_params() and pipeline_prepare() through the pipeline graph. + * + * \param[in] pipeline_id Target pipeline identifier. + * \param[in] rate Target sample rate in Hz (e.g. 16000, 44100, 48000, 96000). + * \return 0 on success, -EBUSY if the pipeline is currently active, + * -EINVAL if the rate is unsupported by component capabilities, + * or negative errno on failure. + */ +int sof_static_pipeline_set_sample_rate(uint32_t pipeline_id, uint32_t rate); + +/** + * \brief Retrieve the current stream sample rate for a specific static pipeline. + * + * \param[in] pipeline_id Target pipeline identifier. + * \return Pipeline sample rate in Hz, or 0 if pipeline not found. + */ +uint32_t sof_static_pipeline_get_sample_rate(uint32_t pipeline_id); + +/** + * \brief Set global sample rate across active static pipelines. + * + * In pipelines with sample rate converters (SRC / ASRC), individual pipeline + * sections maintain their distinct sample rates according to their component + * capabilities and buffer configurations. + * + * \param[in] rate Target primary sample rate in Hz (e.g. 48000). + * \return 0 on success, negative errno on failure. + */ +int sof_static_set_sample_rate(uint32_t rate); + +/** + * \brief Retrieve current primary sample rate. + * \return Current sample rate in Hz. + */ +uint32_t sof_static_get_sample_rate(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __SOF_AUDIO_PIPELINE_STATIC_PIPELINE_H__ */ + +/** @} */ From d819eb5ff4c19d05e2b4fdb8f2601c440803e872 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 11 Oct 2026 13:45:27 +0100 Subject: [PATCH 3/7] audio: pipeline: add generic static module ops registry and IPC4 base config synthesizer Add static_pipeline_modules.c and static_pipeline_modules.h providing generic module operations registration, UUID lookup, and IPC4 base module configuration synthesis. Contains purely generic infrastructure with zero per-module logic and no IPC3 dependencies. Signed-off-by: Liam Girdwood --- src/audio/pipeline/static_pipeline_modules.c | 275 +++++++++++++++++++ src/audio/pipeline/static_pipeline_modules.h | 45 +++ 2 files changed, 320 insertions(+) create mode 100644 src/audio/pipeline/static_pipeline_modules.c create mode 100644 src/audio/pipeline/static_pipeline_modules.h diff --git a/src/audio/pipeline/static_pipeline_modules.c b/src/audio/pipeline/static_pipeline_modules.c new file mode 100644 index 000000000000..e280614782b4 --- /dev/null +++ b/src/audio/pipeline/static_pipeline_modules.c @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +/** + * \file audio/pipeline/static_pipeline_modules.c + * \brief Generic Static Module Operations Registry & Infrastructure + * \author Liam Girdwood + * + * Implements generic module operations registration, lookup, and default + * component initialization helpers for declarative static pipelines. + * This file contains purely generic infrastructure with zero per-module logic. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "static_pipeline_modules.h" + +LOG_MODULE_REGISTER(static_pipeline_modules, CONFIG_SOF_LOG_LEVEL); + +/* ========================================================================= + * Static Module Operations Registry + * ========================================================================= */ + +static APP_SYSUSER_DATA struct list_item s_module_ops_list = LIST_INIT(s_module_ops_list); + +/** + * \brief Register static module operations for an audio component. + * \param[in,out] ops Pointer to module operations structure. + * \return 0 on success, negative error code on failure. + */ +int sof_static_register_module_ops(struct sof_static_module_ops *ops) +{ + if (!ops) { + LOG_ERR("sof_static_register_module_ops: NULL ops pointer"); + return -EINVAL; + } + if (!ops->uuid) { + LOG_ERR("sof_static_register_module_ops: NULL ops UUID"); + return -EINVAL; + } + + list_item_append(&ops->list, &s_module_ops_list); + return 0; +} + +/** + * \brief Find registered static module operations by component UUID. + * \param[in] uuid UUID of target component. + * \return Pointer to registered ops, or NULL if not found or on error. + */ +const struct sof_static_module_ops *sof_static_find_module_ops(const struct sof_uuid *uuid) +{ + if (!uuid) { + LOG_ERR("sof_static_find_module_ops: NULL UUID pointer"); + return NULL; + } + + struct list_item *item; + list_for_item(item, &s_module_ops_list) { + struct sof_static_module_ops *ops = + container_of(item, struct sof_static_module_ops, list); + if (!memcmp(ops->uuid, uuid, UUID_SIZE)) + return ops; + } + + LOG_ERR("sof_static_find_module_ops: no ops registered for requested UUID"); + return NULL; +} + +/* ========================================================================= + * Base IPC4 Configuration & Default Component Creation + * ========================================================================= */ + +/* Channel map slot macro: channel index 'ch' placed at slot index 'slot' */ +#define SOF_CH_MAP_SLOT(slot, ch) (((uint32_t)((ch) & 0xf)) << ((slot) * 4)) + +#define SOF_CH_MAP_1CH(c0) \ + (0xfffffff0 | SOF_CH_MAP_SLOT(0, (c0))) + +#define SOF_CH_MAP_2CH(c0, c1) \ + (0xffffff00 | SOF_CH_MAP_SLOT(0, (c0)) | SOF_CH_MAP_SLOT(1, (c1))) + +#define SOF_CH_MAP_3CH(c0, c1, c2) \ + (0xfffff000 | SOF_CH_MAP_SLOT(0, (c0)) | SOF_CH_MAP_SLOT(1, (c1)) | \ + SOF_CH_MAP_SLOT(2, (c2))) + +#define SOF_CH_MAP_4CH(c0, c1, c2, c3) \ + (0xffff0000 | SOF_CH_MAP_SLOT(0, (c0)) | SOF_CH_MAP_SLOT(1, (c1)) | \ + SOF_CH_MAP_SLOT(2, (c2)) | SOF_CH_MAP_SLOT(3, (c3))) + +#define SOF_CH_MAP_6CH(c0, c1, c2, c3, c4, c5) \ + (0xff000000 | SOF_CH_MAP_SLOT(0, (c0)) | SOF_CH_MAP_SLOT(1, (c1)) | \ + SOF_CH_MAP_SLOT(2, (c2)) | SOF_CH_MAP_SLOT(3, (c3)) | \ + SOF_CH_MAP_SLOT(4, (c4)) | SOF_CH_MAP_SLOT(5, (c5))) + +#define SOF_CH_MAP_8CH(c0, c1, c2, c3, c4, c5, c6, c7) \ + (SOF_CH_MAP_SLOT(0, (c0)) | SOF_CH_MAP_SLOT(1, (c1)) | \ + SOF_CH_MAP_SLOT(2, (c2)) | SOF_CH_MAP_SLOT(3, (c3)) | \ + SOF_CH_MAP_SLOT(4, (c4)) | SOF_CH_MAP_SLOT(5, (c5)) | \ + SOF_CH_MAP_SLOT(6, (c6)) | SOF_CH_MAP_SLOT(7, (c7))) + +void sof_static_init_base_cfg(struct ipc4_base_module_cfg *base_cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us) +{ + if (!base_cfg || !cdesc) { + LOG_ERR("sof_static_init_base_cfg: NULL parameter"); + return; + } + + memset(base_cfg, 0, sizeof(*base_cfg)); + + uint32_t rate = cdesc->caps.default_rate ? cdesc->caps.default_rate : 48000; + uint16_t channels = cdesc->caps.max_channels ? cdesc->caps.max_channels : 2; + + /* + * Validate and clamp channel count before computing buffer sizes (IBS/OBS). + * Unsupported channel counts (> 8 or 0) default to standard stereo (2 channels) + * to ensure internally consistent IBS/OBS metrics and channel mapping metadata. + */ + if (channels == 0 || channels > 8) { + LOG_ERR("sof_static_init_base_cfg: unsupported channel count %u, clamping to stereo", + channels); + channels = 2; + } + + enum sof_ipc_frame fmt = cdesc->caps.default_fmt ? cdesc->caps.default_fmt : SOF_IPC_FRAME_S16_LE; + + uint32_t cont_bytes = get_sample_bytes(fmt); + uint32_t valid_bits = get_sample_bitdepth(fmt); + /* + * Special-case SOF_IPC_FRAME_S16_4LE: + * get_sample_bitdepth() returns 32 because it occupies a 32-bit container, + * but the valid PCM bit depth is 16 bits. + */ + if (fmt == SOF_IPC_FRAME_S16_4LE) + valid_bits = 16; + + uint32_t frames = (rate * (period_us ? period_us : 1000)) / 1000000; + + /* + * Fallback assumption: if integer division resulted in zero frames + * (e.g. invalid rate or ultra-low scheduling period), default to 48 frames, + * representing a standard 1 ms period at 48 kHz (48000 * 1000 / 1000000 = 48). + * Non-zero buffer size is required by IPC4 module infrastructure. + */ + if (frames == 0) + frames = 48; + + base_cfg->cpc = 0; + base_cfg->is_pages = 1; + base_cfg->ibs = frames * channels * cont_bytes; + + /* If sink_rate is specified (e.g. for SRC / ASRC), compute OBS from sink rate */ + if (cdesc->caps.sink_rate) { + uint32_t out_frames = (cdesc->caps.sink_rate * (period_us ? period_us : 1000)) / 1000000; + /* Fallback assumption: same 48-frame minimum for sink buffer */ + if (out_frames == 0) + out_frames = 48; + base_cfg->obs = out_frames * channels * cont_bytes; + } else { + base_cfg->obs = base_cfg->ibs; + } + + base_cfg->audio_fmt.sampling_frequency = rate; + base_cfg->audio_fmt.depth = (cont_bytes * 8); + base_cfg->audio_fmt.valid_bit_depth = valid_bits; + base_cfg->audio_fmt.channels_count = channels; + base_cfg->audio_fmt.interleaving_style = IPC4_CHANNELS_INTERLEAVED; + + /* Map PCM format to IPC4 sample type including unsigned integer for U8 */ + if (fmt == SOF_IPC_FRAME_FLOAT) + base_cfg->audio_fmt.s_type = IPC4_TYPE_FLOAT; + else if (fmt == SOF_IPC_FRAME_A_LAW) + base_cfg->audio_fmt.s_type = IPC4_TYPE_A_LAW; + else if (fmt == SOF_IPC_FRAME_MU_LAW) + base_cfg->audio_fmt.s_type = IPC4_TYPE_MU_LAW; + else if (fmt == SOF_IPC_FRAME_U8) + base_cfg->audio_fmt.s_type = IPC4_TYPE_UNSIGNED_INTEGER; + else + base_cfg->audio_fmt.s_type = IPC4_TYPE_SIGNED_INTEGER; + + /* Configure channel layout and channel slot map matching channel count */ + switch (channels) { + case 1: + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_MONO; + base_cfg->audio_fmt.ch_map = SOF_CH_MAP_1CH(CHANNEL_CENTER); + break; + case 2: + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_STEREO; + base_cfg->audio_fmt.ch_map = SOF_CH_MAP_2CH(CHANNEL_LEFT, CHANNEL_RIGHT); + break; + case 3: + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_3_POINT_0; + base_cfg->audio_fmt.ch_map = SOF_CH_MAP_3CH(CHANNEL_LEFT, CHANNEL_CENTER, + CHANNEL_RIGHT); + break; + case 4: + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_QUATRO; + base_cfg->audio_fmt.ch_map = SOF_CH_MAP_4CH(CHANNEL_LEFT, CHANNEL_RIGHT, + CHANNEL_LEFT_SURROUND, + CHANNEL_RIGHT_SURROUND); + break; + case 5: + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_5_POINT_0; + base_cfg->audio_fmt.ch_map = (0xfff00000 | SOF_CH_MAP_SLOT(0, CHANNEL_LEFT) | + SOF_CH_MAP_SLOT(1, CHANNEL_CENTER) | + SOF_CH_MAP_SLOT(2, CHANNEL_RIGHT) | + SOF_CH_MAP_SLOT(3, CHANNEL_LEFT_SURROUND) | + SOF_CH_MAP_SLOT(4, CHANNEL_RIGHT_SURROUND)); + break; + case 6: + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_5_POINT_1; + base_cfg->audio_fmt.ch_map = SOF_CH_MAP_6CH(CHANNEL_LEFT, CHANNEL_CENTER, + CHANNEL_RIGHT, CHANNEL_LEFT_SURROUND, + CHANNEL_RIGHT_SURROUND, CHANNEL_LFE); + break; + case 7: + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_INVALID; + base_cfg->audio_fmt.ch_map = (0xf0000000 | SOF_CH_MAP_SLOT(0, CHANNEL_LEFT) | + SOF_CH_MAP_SLOT(1, CHANNEL_CENTER) | + SOF_CH_MAP_SLOT(2, CHANNEL_RIGHT) | + SOF_CH_MAP_SLOT(3, CHANNEL_LEFT_SURROUND) | + SOF_CH_MAP_SLOT(4, CHANNEL_RIGHT_SURROUND) | + SOF_CH_MAP_SLOT(5, CHANNEL_LEFT_SIDE) | + SOF_CH_MAP_SLOT(6, CHANNEL_RIGHT_SIDE)); + break; + case 8: + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_7_POINT_1; + base_cfg->audio_fmt.ch_map = SOF_CH_MAP_8CH(CHANNEL_LEFT, CHANNEL_CENTER, + CHANNEL_RIGHT, CHANNEL_LEFT_SURROUND, + CHANNEL_RIGHT_SURROUND, CHANNEL_LFE, + CHANNEL_LEFT_SIDE, CHANNEL_RIGHT_SIDE); + break; + default: + /* Defensive fallback for invalid channel count (clamped upfront) */ + base_cfg->audio_fmt.channels_count = 2; + base_cfg->audio_fmt.ch_cfg = IPC4_CHANNEL_CONFIG_STEREO; + base_cfg->audio_fmt.ch_map = SOF_CH_MAP_2CH(CHANNEL_LEFT, CHANNEL_RIGHT); + break; + } +} + +struct comp_dev *sof_static_module_create_default(const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us) +{ + if (!drv || !cfg || !cdesc) { + LOG_ERR("sof_static_module_create_default: NULL parameter"); + return NULL; + } + + struct ipc4_base_module_cfg base_cfg; + + sof_static_init_base_cfg(&base_cfg, cdesc, period_us); + struct ipc_config_process base_proc_spec = { + .size = sizeof(base_cfg), + .data = (const uint8_t *)&base_cfg, + }; + + return drv->ops.create(drv, cfg, &base_proc_spec); +} diff --git a/src/audio/pipeline/static_pipeline_modules.h b/src/audio/pipeline/static_pipeline_modules.h new file mode 100644 index 000000000000..4ce8195722a5 --- /dev/null +++ b/src/audio/pipeline/static_pipeline_modules.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +/** + * \file audio/pipeline/static_pipeline_modules.h + * \brief Generic internal interfaces for static pipeline module operations. + * \author Liam Girdwood + * + * This private header declares internal helpers shared between the static + * pipeline loader and module adapter implementations. It contains no + * component-specific logic. + */ + +#ifndef __SOF_AUDIO_PIPELINE_STATIC_PIPELINE_MODULES_H__ +#define __SOF_AUDIO_PIPELINE_STATIC_PIPELINE_MODULES_H__ + +#include +#include + +/** + * \brief Synthesize standard IPC4 base module configuration structure. + * \param[out] base_cfg Target base configuration to populate. + * \param[in] cdesc Static component descriptor with format and rate capabilities. + * \param[in] period_us Pipeline scheduling period in microseconds. + */ +void sof_static_init_base_cfg(struct ipc4_base_module_cfg *base_cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us); + +/** + * \brief Default fallback creation for standard IPC4 module adapters. + * \param[in] drv SOF component driver. + * \param[in] cfg IPC component configuration. + * \param[in] cdesc Static component descriptor. + * \param[in] period_us Owning pipeline scheduling period in microseconds. + * \return Created comp_dev pointer, or NULL on failure. + */ +struct comp_dev *sof_static_module_create_default(const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us); + +#endif /* __SOF_AUDIO_PIPELINE_STATIC_PIPELINE_MODULES_H__ */ From e30be015b9063964229c43add761bfecbb3dff6f Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 11 Oct 2026 13:45:35 +0100 Subject: [PATCH 4/7] audio: pipeline: add declarative static topology loader engine Implement static_pipeline_loader.c to parse declarative static audio topologies and instantiate native SOF pipelines, components, intermediate buffers, routes, and kcontrols at boot. Features: - Purely IPC4 based architecture with no IPC3 dependencies - Modular multi-stage initialization with explicit LOG_ERR on all failures - Per-pipeline sample rate negotiation and tracking across pipelines, streams, and DAI/host endpoints - Configurable tracking table limits via Kconfig (STATIC_PIPELINE_MAX_*) to support memory-constrained embedded devices - Runtime kcontrol dispatch to registered module ops with error checking Signed-off-by: Liam Girdwood --- src/audio/Kconfig | 51 + src/audio/pipeline/CMakeLists.txt | 5 + src/audio/pipeline/static_pipeline_loader.c | 1873 +++++++++++++++++++ 3 files changed, 1929 insertions(+) create mode 100644 src/audio/pipeline/static_pipeline_loader.c diff --git a/src/audio/Kconfig b/src/audio/Kconfig index 8accb25738a2..7ea3be039d81 100644 --- a/src/audio/Kconfig +++ b/src/audio/Kconfig @@ -51,6 +51,57 @@ config HOST_DMA_IPC_POSITION_UPDATES by sending an IPC message whenever one period of audio is transferred. Most platforms provide more efficient ways to query the DMA status. +config STATIC_PIPELINE + bool "Declarative Static Audio Pipeline Loader" + depends on IPC_MAJOR_4 + default n + help + Enable support for declarative static audio pipelines in Sound + Open Firmware. This provides a compile-time and runtime loader + engine for hostless, microcontroller, and standalone embedded + targets that do not rely on dynamic IPC topology messages from + a host operating system. + +config STATIC_PIPELINE_MAX_PIPELINES + int "Maximum static pipelines" + default 16 + depends on STATIC_PIPELINE + help + Maximum number of static audio pipelines tracked by the static + pipeline loader. Memory-constrained embedded devices can lower this. + +config STATIC_PIPELINE_MAX_COMPS + int "Maximum static components" + default 64 + depends on STATIC_PIPELINE + help + Maximum number of static audio components tracked by the static + pipeline loader. + +config STATIC_PIPELINE_MAX_BUFFERS + int "Maximum static audio buffers" + default 64 + depends on STATIC_PIPELINE + help + Maximum number of static intermediate audio buffers tracked by + the static pipeline loader. + +config STATIC_PIPELINE_MAX_CONTROLS + int "Maximum static controls" + default 64 + depends on STATIC_PIPELINE + help + Maximum number of static kcontrols tracked by the static + pipeline loader. + +config STATIC_PIPELINE_MAX_ROUTES + int "Maximum static routes" + default 128 + depends on STATIC_PIPELINE + help + Maximum number of static graph connection routes allowed by + the static pipeline loader. + config COMP_CHAIN_DMA bool "Chain DMA component" depends on IPC_MAJOR_4 diff --git a/src/audio/pipeline/CMakeLists.txt b/src/audio/pipeline/CMakeLists.txt index cb26dc3214bd..c812ce02d04a 100644 --- a/src/audio/pipeline/CMakeLists.txt +++ b/src/audio/pipeline/CMakeLists.txt @@ -7,3 +7,8 @@ add_local_sources(sof pipeline-xrun.c pipeline-schedule.c ) + +add_local_sources_ifdef(CONFIG_STATIC_PIPELINE sof + static_pipeline_loader.c + static_pipeline_modules.c +) diff --git a/src/audio/pipeline/static_pipeline_loader.c b/src/audio/pipeline/static_pipeline_loader.c new file mode 100644 index 000000000000..ace6eef3cb9a --- /dev/null +++ b/src/audio/pipeline/static_pipeline_loader.c @@ -0,0 +1,1873 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +/** + * \file audio/pipeline/static_pipeline_loader.c + * \brief Generic Static Audio Topology Loader Engine + * \author Liam Girdwood + * + * Implements the core runtime engine for declarative static audio pipelines. + * In microcontroller, hostless, and standalone embedded environments, this engine + * instantiates SOF components, buffers, routes, and kcontrols directly at boot + * without dynamic IPC topology commands from a host operating system. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../copier/copier.h" +#include "static_pipeline_modules.h" + +LOG_MODULE_REGISTER(static_pipeline_loader, CONFIG_SOF_LOG_LEVEL); + +/* + * Capacity limits for static topology tracking tables. + * Sourced from Kconfig to allow memory-constrained microcontrollers + * and embedded DSPs to scale tracking buffers appropriately. + */ +#ifndef CONFIG_STATIC_PIPELINE_MAX_PIPELINES +#define CONFIG_STATIC_PIPELINE_MAX_PIPELINES 16 +#endif +#ifndef CONFIG_STATIC_PIPELINE_MAX_COMPS +#define CONFIG_STATIC_PIPELINE_MAX_COMPS 64 +#endif +#ifndef CONFIG_STATIC_PIPELINE_MAX_BUFFERS +#define CONFIG_STATIC_PIPELINE_MAX_BUFFERS 64 +#endif +#ifndef CONFIG_STATIC_PIPELINE_MAX_CONTROLS +#define CONFIG_STATIC_PIPELINE_MAX_CONTROLS 64 +#endif +#ifndef CONFIG_STATIC_PIPELINE_MAX_ROUTES +#define CONFIG_STATIC_PIPELINE_MAX_ROUTES 128 +#endif + +#define MAX_STATIC_PIPELINES CONFIG_STATIC_PIPELINE_MAX_PIPELINES +#define MAX_STATIC_COMPS CONFIG_STATIC_PIPELINE_MAX_COMPS +#define MAX_STATIC_BUFFERS CONFIG_STATIC_PIPELINE_MAX_BUFFERS +#define MAX_STATIC_CONTROLS CONFIG_STATIC_PIPELINE_MAX_CONTROLS +#define MAX_STATIC_ROUTES CONFIG_STATIC_PIPELINE_MAX_ROUTES + +/* Active declarative topology descriptor */ +static APP_SYSUSER_DATA const struct sof_static_topology *s_active_topo; + +/* Static tracking tables for fast ID-to-pointer lookups */ +static APP_SYSUSER_BSS struct pipeline *s_pipelines[MAX_STATIC_PIPELINES]; +static APP_SYSUSER_BSS uint32_t s_pipeline_ids[MAX_STATIC_PIPELINES]; +static APP_SYSUSER_BSS uint32_t s_pipeline_rates[MAX_STATIC_PIPELINES]; +static APP_SYSUSER_BSS size_t s_num_pipelines; + +static APP_SYSUSER_BSS struct comp_dev *s_comps[MAX_STATIC_COMPS]; +static APP_SYSUSER_BSS uint32_t s_comp_ids[MAX_STATIC_COMPS]; +static APP_SYSUSER_BSS size_t s_num_comps; + +static APP_SYSUSER_BSS struct comp_buffer *s_buffers[MAX_STATIC_BUFFERS]; +static APP_SYSUSER_BSS uint32_t s_buffer_ids[MAX_STATIC_BUFFERS]; +static APP_SYSUSER_BSS size_t s_num_buffers; + +/* Cached kcontrol values */ +static APP_SYSUSER_BSS int32_t s_control_vals[MAX_STATIC_CONTROLS]; + +/** + * \brief Calculate valid sample byte size for a given PCM frame format. + * + * Handles special case SOF_IPC_FRAME_S16_4LE where 16 valid bits reside in a + * 32-bit container, returning 2 valid bytes rather than container size 4. + * + * \param[in] fmt PCM frame format enum. + * \return Valid sample bytes (e.g. 2 for S16/S16_4LE, 3 for S24_3LE, 4 for S24_4LE/S32/FLOAT). + */ +static inline uint32_t static_get_sample_valid_bytes(enum sof_ipc_frame fmt) +{ + if (fmt == SOF_IPC_FRAME_S16_4LE) + return 2; + return get_sample_bitdepth(fmt) / 8; +} + +/** + * \brief Convert sample rate in Hz to capability bitmask. + * \param[in] rate Sample rate in Hz. + * \return Matching SOF_STATIC_RATE_* bitmask or 0. + */ +static inline uint32_t sof_static_rate_to_mask(uint32_t rate) +{ + /* Map standard audio sampling rates to static capability bitmasks */ + switch (rate) { + case 8000: return SOF_STATIC_RATE_8K; + case 11025: return SOF_STATIC_RATE_11K; + case 12000: return SOF_STATIC_RATE_12K; + case 16000: return SOF_STATIC_RATE_16K; + case 22050: return SOF_STATIC_RATE_22K; + case 24000: return SOF_STATIC_RATE_24K; + case 32000: return SOF_STATIC_RATE_32K; + case 44100: return SOF_STATIC_RATE_44K1; + case 48000: return SOF_STATIC_RATE_48K; + case 64000: return SOF_STATIC_RATE_64K; + case 88200: return SOF_STATIC_RATE_88K2; + case 96000: return SOF_STATIC_RATE_96K; + case 176400: return SOF_STATIC_RATE_176K4; + case 192000: return SOF_STATIC_RATE_192K; + case 384000: return SOF_STATIC_RATE_384K; + default: return 0; + } +} + +/** + * \brief Helper to find declarative component descriptor by unique component ID. + * \param[in] topo Active topology pointer. + * \param[in] comp_id Target component identifier. + * \return Pointer to matched struct sof_static_comp or NULL. + */ +static const struct sof_static_comp * +sof_static_find_comp_desc(const struct sof_static_topology *topo, uint32_t comp_id) +{ + if (!topo) + return NULL; + + /* Linear search across declared components */ + for (size_t c = 0; c < topo->num_comps; c++) { + if (topo->comps[c].id == comp_id) + return &topo->comps[c]; + } + return NULL; +} + +/** + * \brief Helper to find declarative pipeline descriptor by unique pipeline ID. + * \param[in] topo Active topology pointer. + * \param[in] pipeline_id Target pipeline identifier. + * \return Pointer to matched struct sof_static_pipeline_desc or NULL. + */ +static const struct sof_static_pipeline_desc * +sof_static_find_pipeline_desc(const struct sof_static_topology *topo, uint32_t pipeline_id) +{ + if (!topo) + return NULL; + + for (size_t p = 0; p < topo->num_pipelines; p++) { + if (topo->pipelines[p].pipeline_id == pipeline_id) + return &topo->pipelines[p]; + } + return NULL; +} + +/** + * \brief Send initial binary configuration blob (e.g. EQ coefficients, DRC tuning) to a component. + * + * In hostless environments, modules requiring configuration tables (such as equalizer + * filter coefficients or DRC curves) must be configured directly at boot. This routine + * dispatches the initial configuration blob via the IPC4 module adapter's set_configuration ops. + * + * \param[in] dev Target component device pointer. + * \param[in] abi_blob Pointer to ABI configuration blob starting with struct sof_abi_hdr. + * \param[in] abi_blob_total_size Total size of blob in bytes including header. + * \return 0 on success, negative errno on failure. + */ +static int send_comp_config(struct comp_dev *dev, const void *abi_blob, size_t abi_blob_total_size) +{ + /* Validate component pointer and verify minimum blob size covers ABI header */ + if (!dev || !abi_blob || abi_blob_total_size < sizeof(struct sof_abi_hdr)) { + LOG_ERR("send_comp_config: invalid parameter or truncated blob (size %zu)", + abi_blob_total_size); + return -EINVAL; + } + + const struct sof_abi_hdr *hdr = (const struct sof_abi_hdr *)abi_blob; + + /* Verify payload length fits within supplied total size */ + if (hdr->size > abi_blob_total_size - sizeof(struct sof_abi_hdr)) { + LOG_ERR("send_comp_config: payload size %u exceeds total size %zu", + hdr->size, abi_blob_total_size); + return -EINVAL; + } + + struct processing_module *mod = comp_mod(dev); + + /* Verify the component's module adapter driver provides set_configuration */ + if (!mod || !mod->dev || !mod->dev->drv || !mod->dev->drv->adapter_ops || + !mod->dev->drv->adapter_ops->set_configuration) { + LOG_ERR("No adapter_ops set_configuration for comp %d", dev->ipc_config.id); + return -EINVAL; + } + + /* Pass raw configuration payload directly following ABI header to module adapter */ + const uint8_t *raw_data = (const uint8_t *)abi_blob + sizeof(struct sof_abi_hdr); + int ret = mod->dev->drv->adapter_ops->set_configuration(mod, 0, MODULE_CFG_FRAGMENT_SINGLE, + hdr->size, raw_data, + hdr->size, NULL, 0); + + /* Log configuration status for diagnostics */ + if (ret < 0) + LOG_ERR("set_configuration failed for comp %d: ret %d", dev->ipc_config.id, ret); + else + LOG_INF("Configuration blob loaded for comp %d (%u bytes)", dev->ipc_config.id, hdr->size); + + return ret; +} + +/** + * \brief Initialize audio stream and buffer parameters for an intermediate connection buffer. + * + * Configures the IPC stream parameters (sampling rate, channel count, frame format, + * container sizes, valid bit depth, host period bytes, and ALSA channel map) and + * synchronizes the underlying audio stream (buf->stream) as well as its associated + * sink and source interfaces. + * + * Intermediate circular buffers connect adjacent processing components in the + * graph. Initializing their stream parameters and reset state ensures that + * buffer_set_params() and sink/source getters establish identical format + * expectations prior to component parameter negotiation and streaming. + * + * \param[in,out] buf Pointer to allocated intermediate component buffer. + * \param[in] dir Audio stream direction (SOF_IPC_STREAM_PLAYBACK or SOF_IPC_STREAM_CAPTURE). + * \param[in] fmt PCM audio frame format (enum sof_ipc_frame). + * \param[in] rate Audio sampling rate in Hz. + * \param[in] channels Audio channel count. + */ +static void init_buffer_params(struct comp_buffer *buf, uint32_t dir, enum sof_ipc_frame fmt, + uint32_t rate, uint32_t channels) +{ + if (!buf) + return; + + /* Calculate sample container and valid byte sizes based on the PCM format */ + uint32_t sample_bytes = get_sample_bytes(fmt); + uint32_t valid_bytes = static_get_sample_valid_bytes(fmt); + + /* Number of audio frames per 1 millisecond scheduling period */ + uint32_t frames = rate / 1000; + + /* Populate standard IPC stream parameters structure */ + struct sof_ipc_stream_params params; + memset(¶ms, 0, sizeof(params)); + params.rate = rate; + params.channels = channels; + params.frame_fmt = fmt; + params.sample_container_bytes = sample_bytes; + params.sample_valid_bytes = valid_bytes; + params.buffer_fmt = SOF_IPC_BUFFER_INTERLEAVED; + params.host_period_bytes = frames * channels * sample_bytes; + params.direction = dir; + + /* Populate standard channel mapping for mono, stereo, and multi-channel streams */ + for (uint32_t c = 0; c < SOF_IPC_MAX_CHANNELS; c++) { + if (c == 0) + params.chmap[c] = (channels == 1) ? SOF_CHMAP_MONO : SOF_CHMAP_FL; + else if (c == 1 && channels > 1) + params.chmap[c] = SOF_CHMAP_FR; + else if (c < channels) + params.chmap[c] = c + 1; + else + params.chmap[c] = SOF_CHMAP_NA; + } + + /* Apply stream parameters to buffer structure */ + buffer_set_params(buf, ¶ms, BUFFER_UPDATE_FORCE); + + /* Configure circular audio stream parameters and byte alignment */ + audio_stream_set_valid_fmt(&buf->stream, fmt); + audio_stream_set_rate(&buf->stream, rate); + audio_stream_set_channels(&buf->stream, channels); + audio_stream_set_frm_fmt(&buf->stream, fmt); + audio_stream_set_align(SOF_FRAME_BYTE_ALIGN, sample_bytes, &buf->stream); + audio_stream_reset(&buf->stream); + + /* Synchronize sink interface parameters so downstream consumers match */ + struct sof_sink *sink = audio_buffer_get_sink(&buf->audio_buffer); + if (sink) { + sink_set_valid_fmt(sink, fmt); + sink_set_rate(sink, rate); + sink_set_channels(sink, channels); + sink_set_frm_fmt(sink, fmt); + sink_set_alignment_constants(sink, SOF_FRAME_BYTE_ALIGN, sample_bytes); + } + + /* Synchronize source interface parameters so upstream producers match */ + struct sof_source *source = audio_buffer_get_source(&buf->audio_buffer); + if (source) { + source_set_valid_fmt(source, fmt); + source_set_rate(source, rate); + source_set_channels(source, channels); + source_set_frm_fmt(source, fmt); + source_set_alignment_constants(source, SOF_FRAME_BYTE_ALIGN, sample_bytes); + } +} + +/** + * \brief Stage 1: Allocate top-level pipelines. + * + * Allocate pipeline objects first because components and buffers require an + * owning pipeline context for scheduling, buffer allocation, and clock domains. + * Each pipeline maintains its own sampling rate, scheduling period, and time domain. + * + * \param[in] topo Topology descriptor pointer. + * \return 0 on success, negative errno on failure. + */ +static int sof_static_topology_init_pipelines(const struct sof_static_topology *topo) +{ + for (size_t i = 0; i < topo->num_pipelines && i < MAX_STATIC_PIPELINES; i++) { + const struct sof_static_pipeline_desc *pdesc = &topo->pipelines[i]; + struct pipeline *pipe = pipeline_new(NULL, pdesc->pipeline_id, pdesc->priority, + pdesc->pipeline_id, NULL); + if (!pipe) { + LOG_ERR("Failed to allocate pipeline %u (%s)", pdesc->pipeline_id, pdesc->name); + return -ENOMEM; + } + + /* Configure pipeline scheduling parameters from declarative descriptor */ + pipe->pipeline_id = pdesc->pipeline_id; + pipe->core = pdesc->core; + pipe->period = pdesc->period ? pdesc->period : 1000; + pipe->frames_per_sched = pdesc->frames_per_sched ? pdesc->frames_per_sched : 48; + pipe->time_domain = pdesc->time_domain; + + /* Record pipeline instance and sample rate in static tracking arrays */ + s_pipelines[s_num_pipelines] = pipe; + s_pipeline_ids[s_num_pipelines] = pdesc->pipeline_id; + s_pipeline_rates[s_num_pipelines] = pdesc->default_rate ? pdesc->default_rate : 48000; + s_num_pipelines++; + + LOG_INF("Pipeline %u ('%s') initialized (period %u us, %u frames, %u Hz)", + pdesc->pipeline_id, pdesc->name ? pdesc->name : "", pipe->period, + pipe->frames_per_sched, s_pipeline_rates[s_num_pipelines - 1]); + } + + return 0; +} + +/** + * \brief Instantiate a hardware DAI endpoint component. + * + * \param[in] cdesc Component descriptor. + * \param[in] drv Component driver pointer. + * \param[in] cfg Standard component IPC configuration. + * \return Pointer to created comp_dev, or NULL on failure. + */ +static struct comp_dev *sof_static_init_dai_comp(const struct sof_static_comp *cdesc, + const struct comp_driver *drv, + const struct comp_ipc_config *cfg, + const struct sof_static_topology *topo) +{ + /* Validate component and driver descriptor pointers */ + if (!cdesc || !drv || !cfg) { + LOG_ERR("sof_static_init_dai_comp: invalid NULL arguments"); + return NULL; + } + + const struct sof_static_pipeline_desc *pdesc = + sof_static_find_pipeline_desc(topo, cdesc->pipeline_id); + + /* Derive DAI sampling frequency from component caps or owning pipeline rate */ + uint32_t dai_rate = cdesc->caps.default_rate; + if (!dai_rate && pdesc) + dai_rate = pdesc->default_rate; + if (!dai_rate) + dai_rate = 48000; + + struct ipc_config_dai dai_cfg = { + .type = cdesc->ep.dai.dai_type, + .dai_index = cdesc->ep.dai.dai_index, + .direction = cdesc->direction, + .sampling_frequency = dai_rate, + .dma_buffer_size = 1024, + .format = cdesc->ep.dai.format, + }; + struct comp_dev *dev = drv->ops.create(drv, cfg, &dai_cfg); + + if (!dev) { + LOG_ERR("Failed to create DAI component %u ('%s')", cdesc->id, cdesc->name); + return NULL; + } + + /* Synthesize IPC4 copier and gateway configuration expected by IPC4 DAI driver */ + struct ipc4_copier_module_cfg copier_cfg; + memset(&copier_cfg, 0, sizeof(copier_cfg)); + sof_static_init_base_cfg(&copier_cfg.base, cdesc, pdesc ? pdesc->period : 1000); + copier_cfg.out_fmt = copier_cfg.base.audio_fmt; + + enum ipc4_connector_node_id_type node_type; + switch (cdesc->ep.dai.dai_type) { + case SOF_DAI_INTEL_SSP: + node_type = (cdesc->direction == SOF_IPC_STREAM_PLAYBACK) ? + ipc4_i2s_link_output_class : ipc4_i2s_link_input_class; + break; + case SOF_DAI_INTEL_DMIC: + node_type = ipc4_dmic_link_input_class; + break; + case SOF_DAI_INTEL_HDA: + node_type = (cdesc->direction == SOF_IPC_STREAM_PLAYBACK) ? + ipc4_hda_link_output_class : ipc4_hda_link_input_class; + break; + case SOF_DAI_INTEL_ALH: + node_type = (cdesc->direction == SOF_IPC_STREAM_PLAYBACK) ? + ipc4_alh_link_output_class : ipc4_alh_link_input_class; + break; + default: + node_type = (cdesc->direction == SOF_IPC_STREAM_PLAYBACK) ? + ipc4_i2s_link_output_class : ipc4_i2s_link_input_class; + break; + } + + copier_cfg.gtw_cfg.node_id.f.dma_type = node_type; + copier_cfg.gtw_cfg.node_id.f.v_index = cdesc->ep.dai.dai_index; + copier_cfg.gtw_cfg.dma_buffer_size = 1024; + copier_cfg.gtw_cfg.config_length = 0; + + /* Invoke optional platform/endpoint-specific configuration callback */ + if (cdesc->ep.dai.configure) { + int ret_cfg = cdesc->ep.dai.configure(dev, &dai_cfg, &copier_cfg); + if (ret_cfg < 0) { + LOG_ERR("DAI endpoint configure callback failed %d for comp %u", + ret_cfg, cdesc->id); + return NULL; + } + } + + /* Apply DAI configuration to driver data via the component driver framework */ + struct dai_data *dd = comp_get_drvdata(dev); + + if (dd) { + int ret_dai = comp_dai_config(dd, dev, &dai_cfg, &copier_cfg); + if (ret_dai < 0) { + LOG_ERR("comp_dai_config failed %d for comp %u", ret_dai, cdesc->id); + return NULL; + } + } + + return dev; +} + +/** + * \brief Instantiate a host/USB endpoint component. + * + * Populates struct ipc_config_host expected by the host component driver + * (comp_host::create / host_new) rather than using module adapter fallback. + * + * \param[in] cdesc Component descriptor. + * \param[in] drv Component driver pointer. + * \param[in] cfg Standard component IPC configuration. + * \return Pointer to created comp_dev, or NULL on failure. + */ +static struct comp_dev *sof_static_init_host_comp(const struct sof_static_comp *cdesc, + const struct comp_driver *drv, + const struct comp_ipc_config *cfg) +{ + /* Validate component and driver descriptor pointers */ + if (!cdesc || !drv || !cfg) { + LOG_ERR("sof_static_init_host_comp: invalid NULL arguments"); + return NULL; + } + + /* Populate host driver configuration */ + struct ipc_config_host host_cfg = { + .direction = cdesc->direction, + .no_irq = 0, + .dmac_config = 0, + .dma_buffer_size = 1024, + .feature_mask = 0, + }; + + /* Invoke host driver create callback */ + struct comp_dev *dev = drv->ops.create(drv, cfg, &host_cfg); + if (!dev) { + LOG_ERR("Failed to create host component %u ('%s')", cdesc->id, cdesc->name); + return NULL; + } + + return dev; +} + +/** + * \brief Instantiate an audio processing module or host endpoint component. + * + * \param[in] cdesc Component descriptor. + * \param[in] drv Component driver pointer. + * \param[in] cfg Standard component IPC configuration. + * \param[in] topo Topology descriptor pointer. + * \return Pointer to created comp_dev, or NULL on failure. + */ +static struct comp_dev *sof_static_init_module_comp(const struct sof_static_comp *cdesc, + const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_topology *topo) +{ + const struct sof_static_pipeline_desc *pdesc = NULL; + + for (size_t p = 0; p < topo->num_pipelines; p++) { + if (topo->pipelines[p].pipeline_id == cdesc->pipeline_id) { + pdesc = &topo->pipelines[p]; + break; + } + } + uint32_t period_us = (pdesc && pdesc->period) ? pdesc->period : 1000; + + /* Delegate instantiation to registered static ops or generic fallback */ + const struct sof_static_module_ops *ops = sof_static_find_module_ops(cdesc->uuid); + + if (ops && ops->create) + return ops->create(drv, cfg, cdesc, period_us); + + return sof_static_module_create_default(drv, cfg, cdesc, period_us); +} + +/** + * \brief Stage 2: Instantiate components. + * + * Create all declared audio processing modules, DAI endpoints, and host/USB + * interfaces. Look up drivers in the global SOF registry and initialize them. + * + * \param[in] topo Topology descriptor pointer. + * \return 0 on success, negative errno on failure. + */ +static int sof_static_topology_init_comps(const struct sof_static_topology *topo) +{ + for (size_t i = 0; i < topo->num_comps && i < MAX_STATIC_COMPS; i++) { + const struct sof_static_comp *cdesc = &topo->comps[i]; + uint32_t drv_type = (cdesc->type == SOF_STATIC_COMP_HOST) ? SOF_COMP_HOST : + (cdesc->type == SOF_STATIC_COMP_DAI) ? SOF_COMP_DAI : + SOF_COMP_MODULE_ADAPTER; + + /* Look up driver from component driver registry using standard API */ + const struct comp_driver *drv = comp_driver_find(cdesc->uuid, drv_type); + if (!drv) { + LOG_ERR("Component driver not found for comp %u ('%s')", cdesc->id, cdesc->name); + return -ENODEV; + } + + /* Find owning pipeline descriptor to resolve core affinity and scheduling */ + const struct sof_static_pipeline_desc *pdesc = + sof_static_find_pipeline_desc(topo, cdesc->pipeline_id); + uint32_t comp_core = pdesc ? pdesc->core : 0; + + /* Populate standard SOF component IPC configuration */ + struct comp_ipc_config cfg = { + .id = cdesc->id, + .pipeline_id = cdesc->pipeline_id, + .core = comp_core, + .proc_domain = COMP_PROCESSING_DOMAIN_LL, + .frame_fmt = cdesc->caps.default_fmt, + .type = drv_type, + }; + + struct comp_dev *dev; + + if (cdesc->type == SOF_STATIC_COMP_DAI) + dev = sof_static_init_dai_comp(cdesc, drv, &cfg, topo); + else if (cdesc->type == SOF_STATIC_COMP_HOST) + dev = sof_static_init_host_comp(cdesc, drv, &cfg); + else + dev = sof_static_init_module_comp(cdesc, drv, &cfg, topo); + + if (!dev) { + LOG_ERR("Failed to create component %u ('%s')", cdesc->id, cdesc->name); + return -ENOMEM; + } + + /* Set component stream direction and owning pipeline */ + dev->direction = cdesc->direction; + dev->pipeline = sof_static_pipeline_get(cdesc->pipeline_id); + + /* Assign scheduling period from owning pipeline descriptor */ + dev->period = (pdesc && pdesc->period) ? pdesc->period : 1000; + + /* Derive component frames per scheduling period */ + uint32_t comp_rate = cdesc->caps.default_rate ? cdesc->caps.default_rate : + ((pdesc && pdesc->default_rate) ? pdesc->default_rate : + 48000); + dev->frames = (comp_rate * dev->period) / 1000000; + if (dev->frames == 0) + dev->frames = 48; + + /* Send initial configuration blob if specified (EQ, DRC, etc.) */ + if (cdesc->init_blob && cdesc->init_blob_size > 0) { + int ret_blob = send_comp_config(dev, cdesc->init_blob, cdesc->init_blob_size); + if (ret_blob < 0) { + LOG_ERR("Failed to send init blob for component %u: %d", cdesc->id, ret_blob); + return ret_blob; + } + } + + /* Record component instance in static tracking array */ + s_comps[s_num_comps] = dev; + s_comp_ids[s_num_comps] = cdesc->id; + s_num_comps++; + + LOG_INF("Component %u ('%s') created in pipeline %u", cdesc->id, cdesc->name, cdesc->pipeline_id); + } + + return 0; +} + +/** + * \brief Stage 3: Allocate audio buffers. + * + * Allocate intermediate circular buffers linking components. Each buffer inherits + * its rate, channels, format, and stream direction from its connected producer component. + * + * \param[in] topo Topology descriptor pointer. + * \return 0 on success, negative errno on failure. + */ +static int sof_static_topology_init_buffers(const struct sof_static_topology *topo) +{ + for (size_t i = 0; i < topo->num_buffers && i < MAX_STATIC_BUFFERS; i++) { + const struct sof_static_buffer *bdesc = &topo->buffers[i]; + struct comp_buffer *buf = buffer_alloc(NULL, bdesc->size, bdesc->flags, + PLATFORM_DCACHE_ALIGN, false); + if (!buf) { + LOG_ERR("Failed to allocate buffer %u (size %zu)", bdesc->id, bdesc->size); + return -ENOMEM; + } + + /* Resolve buffer parameters: rate, channels, format, and stream direction */ + uint32_t rate = bdesc->rate; + uint16_t channels = bdesc->channels; + enum sof_ipc_frame fmt = bdesc->fmt; + uint32_t dir = SOF_IPC_STREAM_PLAYBACK; + + /* Look up the producer component connected to this buffer via route table */ + const struct sof_static_comp *prod_comp = NULL; + for (size_t r = 0; r < topo->num_routes; r++) { + if (topo->routes[r].buffer_id == bdesc->id) { + uint32_t src_id = topo->routes[r].src_comp_id; + for (size_t c = 0; c < topo->num_comps; c++) { + if (topo->comps[c].id == src_id) { + prod_comp = &topo->comps[c]; + break; + } + } + break; + } + } + + /* Inherit parameters from producer component */ + if (prod_comp) { + dir = prod_comp->direction; + if (fmt == SOF_IPC_FRAME_INVALID) + fmt = prod_comp->caps.default_fmt; + if (!rate) { + /* If producer is a rate converter (SRC / ASRC), inherit sink_rate */ + if (prod_comp->caps.sink_rate) + rate = prod_comp->caps.sink_rate; + else + rate = prod_comp->caps.default_rate; + } + if (!channels) + channels = prod_comp->caps.max_channels; + } + + /* If rate or channels still unassigned, inherit from producer's owning pipeline */ + if (!rate && prod_comp) { + const struct sof_static_pipeline_desc *pdesc = + sof_static_find_pipeline_desc(topo, prod_comp->pipeline_id); + if (pdesc && pdesc->default_rate) + rate = pdesc->default_rate; + } + + /* Default fallbacks if unassigned */ + if (!rate) + rate = 48000; + if (!channels) + channels = 2; + if (fmt == SOF_IPC_FRAME_INVALID) + fmt = SOF_IPC_FRAME_S16_LE; + + /* Initialize circular buffer stream parameters and reset pointers */ + init_buffer_params(buf, dir, fmt, rate, channels); + + /* Record buffer instance in static tracking array */ + s_buffers[s_num_buffers] = buf; + s_buffer_ids[s_num_buffers] = bdesc->id; + s_num_buffers++; + } + + return 0; +} + +/** + * \brief Stage 4: Connect pipeline graph routes. + * + * Establish graph connections: producer comp -> intermediate buffer -> consumer comp. + * + * \param[in] topo Topology descriptor pointer. + * \return 0 on success, negative errno on failure. + */ +static int sof_static_topology_init_routes(const struct sof_static_topology *topo) +{ + for (size_t i = 0; i < topo->num_routes; i++) { + const struct sof_static_route *r = &topo->routes[i]; + struct comp_dev *src = sof_static_comp_get(r->src_comp_id); + struct comp_buffer *buf = sof_static_buffer_get(r->buffer_id); + struct comp_dev *sink = sof_static_comp_get(r->sink_comp_id); + + /* Verify all route endpoints exist */ + if (!src || !buf || !sink) { + LOG_ERR("Failed route %u -> [buf %u] -> %u", r->src_comp_id, r->buffer_id, r->sink_comp_id); + return -EINVAL; + } + + /* Connect producer component to buffer and buffer to consumer component */ + int ret = pipeline_connect(src, buf, PPL_CONN_DIR_COMP_TO_BUFFER); + if (ret < 0) { + LOG_ERR("Failed pipeline_connect src %u -> buf %u: %d", + r->src_comp_id, r->buffer_id, ret); + return ret; + } + + ret = pipeline_connect(sink, buf, PPL_CONN_DIR_BUFFER_TO_COMP); + if (ret < 0) { + LOG_ERR("Failed pipeline_connect buf %u -> sink %u: %d", + r->buffer_id, r->sink_comp_id, ret); + return ret; + } + + LOG_DBG("Connected: %u -> [buf %u] -> %u", r->src_comp_id, r->buffer_id, r->sink_comp_id); + } + + return 0; +} + +/** + * \brief Stage 5: Set pipeline endpoints and propagate parameters. + * + * Bind source, sink, and scheduling components to each pipeline. + * Propagate pipeline_params() and pipeline_prepare() to initiate graph-wide + * parameter negotiation across constituent components and buffers. + * + * \param[in] topo Topology descriptor pointer. + * \return 0 on success, negative errno on failure. + */ +static int sof_static_topology_init_pipeline_endpoints(const struct sof_static_topology *topo) +{ + for (size_t i = 0; i < topo->num_pipelines; i++) { + const struct sof_static_pipeline_desc *pdesc = &topo->pipelines[i]; + struct pipeline *pipe = sof_static_pipeline_get(pdesc->pipeline_id); + if (!pipe) { + LOG_ERR("Pipeline %u not found", pdesc->pipeline_id); + return -EINVAL; + } + + /* Bind primary source, sink, and scheduling components */ + pipe->source_comp = sof_static_comp_get(pdesc->source_comp_id); + pipe->sink_comp = sof_static_comp_get(pdesc->sink_comp_id); + pipe->sched_comp = sof_static_comp_get(pdesc->sched_comp_id); + + if (!pipe->source_comp || !pipe->sink_comp) { + LOG_ERR("Pipeline %u missing primary source (ID %u) or sink (ID %u)", + pdesc->pipeline_id, pdesc->source_comp_id, pdesc->sink_comp_id); + return -EINVAL; + } + + /* Resolve the component driving pipeline parameters */ + struct comp_dev *host_or_sched = pipe->sched_comp ? pipe->sched_comp : pipe->source_comp; + if (host_or_sched) { + const struct sof_static_comp *cdesc = NULL; + for (size_t c = 0; c < topo->num_comps; c++) { + if (topo->comps[c].id == dev_comp_id(host_or_sched)) { + cdesc = &topo->comps[c]; + break; + } + } + + /* Determine stream sampling rate for this pipeline */ + uint32_t rate = pdesc->default_rate; + if (!rate && cdesc) + rate = cdesc->caps.default_rate; + if (!rate) + rate = 48000; + + /* Determine stream channel count */ + uint32_t channels = pdesc->default_channels; + if (!channels && cdesc) + channels = cdesc->caps.max_channels; + if (!channels) + channels = 2; + + /* Determine stream PCM frame format */ + enum sof_ipc_frame fmt = (cdesc && cdesc->caps.default_fmt) ? + cdesc->caps.default_fmt : host_or_sched->ipc_config.frame_fmt; + if (!fmt) + fmt = SOF_IPC_FRAME_S16_LE; + + /* Calculate container bytes, valid bytes, and period frame size */ + uint32_t cont_bytes = get_sample_bytes(fmt); + uint32_t valid_bytes = static_get_sample_valid_bytes(fmt); + uint32_t period_us = pdesc->period ? pdesc->period : 1000; + uint32_t frames = pdesc->frames_per_sched ? pdesc->frames_per_sched : + ((rate * period_us) / 1000000); + if (frames == 0) + frames = 48; + + /* Populate PCM params structure for the pipeline */ + struct sof_ipc_pcm_params prms; + memset(&prms, 0, sizeof(prms)); + prms.params.rate = rate; + prms.params.channels = channels; + prms.params.frame_fmt = fmt; + prms.params.sample_container_bytes = cont_bytes; + prms.params.sample_valid_bytes = valid_bytes; + prms.params.buffer_fmt = SOF_IPC_BUFFER_INTERLEAVED; + prms.params.host_period_bytes = frames * channels * cont_bytes; + prms.comp_id = dev_comp_id(host_or_sched); + prms.params.direction = pdesc->direction; + for (uint32_t c = 0; c < SOF_IPC_MAX_CHANNELS; c++) { + if (c == 0) + prms.params.chmap[c] = (channels == 1) ? SOF_CHMAP_MONO : SOF_CHMAP_FL; + else if (c == 1 && channels > 1) + prms.params.chmap[c] = SOF_CHMAP_FR; + else if (c < channels) + prms.params.chmap[c] = c + 1; + else + prms.params.chmap[c] = SOF_CHMAP_NA; + } + + /* Propagate params and prepare throughout the pipeline walk */ + int ret_prms = pipeline_params(pipe, host_or_sched, &prms); + if (ret_prms < 0) { + LOG_ERR("Pipeline %u params failed: %d", pdesc->pipeline_id, ret_prms); + return ret_prms; + } + int ret_prep = pipeline_prepare(pipe, host_or_sched); + if (ret_prep < 0) { + LOG_ERR("Pipeline %u prepare failed: %d", pdesc->pipeline_id, ret_prep); + return ret_prep; + } + LOG_INF("Pipeline %u params (rate %u, ch %u, fmt %u) OK, prepare OK", + pdesc->pipeline_id, rate, channels, fmt); + } + } + + return 0; +} + +/** + * \brief Stage 6: Explicitly configure and prepare all components. + * + * Ensure every instantiated component transitions to COMP_STATE_PREPARE, + * even if not reached by the initial pipeline walk. This guarantees processing + * modules (e.g. Volume, EQ, DRC, Selector) are fully primed and ready for streaming. + * + * \param[in] topo Topology descriptor pointer. + * \return 0 on success, negative errno on failure. + */ +static int sof_static_topology_prepare_comps(const struct sof_static_topology *topo) +{ + for (size_t i = 0; i < topo->num_comps; i++) { + struct comp_dev *dev = sof_static_comp_get(topo->comps[i].id); + if (!dev) + continue; + + /* Only prepare components currently in READY state */ + if (dev->state == COMP_STATE_READY) { + const struct sof_static_comp *cdesc = &topo->comps[i]; + const struct sof_static_pipeline_desc *pdesc = + sof_static_find_pipeline_desc(topo, cdesc->pipeline_id); + + /* Determine component-specific sample rate */ + uint32_t rate = cdesc->caps.default_rate; + if (!rate && pdesc) + rate = pdesc->default_rate; + if (!rate) + rate = 48000; + + /* Determine channel count */ + uint32_t channels = cdesc->caps.max_channels; + if (!channels && pdesc) + channels = pdesc->default_channels; + if (!channels) + channels = 2; + + /* Determine frame format */ + enum sof_ipc_frame fmt = cdesc->caps.default_fmt ? cdesc->caps.default_fmt : + dev->ipc_config.frame_fmt; + if (!fmt) + fmt = SOF_IPC_FRAME_S16_LE; + + /* Calculate container size, valid bits, and frames */ + uint32_t cont_bytes = get_sample_bytes(fmt); + uint32_t valid_bytes = static_get_sample_valid_bytes(fmt); + uint32_t period_us = (pdesc && pdesc->period) ? pdesc->period : 1000; + uint32_t frames = (pdesc && pdesc->frames_per_sched) ? pdesc->frames_per_sched : + ((rate * period_us) / 1000000); + if (frames == 0) + frames = 48; + + /* Populate component PCM parameters */ + struct sof_ipc_pcm_params prms; + memset(&prms, 0, sizeof(prms)); + prms.params.rate = rate; + prms.params.channels = channels; + prms.params.frame_fmt = fmt; + prms.params.sample_container_bytes = cont_bytes; + prms.params.sample_valid_bytes = valid_bytes; + prms.params.buffer_fmt = SOF_IPC_BUFFER_INTERLEAVED; + prms.params.host_period_bytes = frames * channels * cont_bytes; + prms.comp_id = dev_comp_id(dev); + prms.params.direction = dev->direction; + for (uint32_t c = 0; c < SOF_IPC_MAX_CHANNELS; c++) { + if (c == 0) + prms.params.chmap[c] = (channels == 1) ? SOF_CHMAP_MONO : SOF_CHMAP_FL; + else if (c == 1 && channels > 1) + prms.params.chmap[c] = SOF_CHMAP_FR; + else if (c < channels) + prms.params.chmap[c] = c + 1; + else + prms.params.chmap[c] = SOF_CHMAP_NA; + } + + /* Call component params and prepare callbacks */ + int ret_prms = comp_params(dev, &prms.params); + if (ret_prms < 0) { + LOG_ERR("Component %u ('%s') params failed: %d", + dev_comp_id(dev), cdesc->name, ret_prms); + return ret_prms; + } + int ret_prep = comp_prepare(dev); + if (ret_prep < 0) { + LOG_ERR("Component %u ('%s') prepare failed: %d", + dev_comp_id(dev), cdesc->name, ret_prep); + return ret_prep; + } + LOG_INF("Component %u ('%s') explicit prepare OK (state %d)", + dev_comp_id(dev), cdesc->name, dev->state); + } + } + + return 0; +} + +/** + * \brief Stage 7: Initialize kcontrols. + * + * Set up all declared volume, mute, enum, and binary kcontrols with their + * default boot values so processing modules start in their expected state. + * + * \param[in] topo Topology descriptor pointer. + * \return 0 on success, negative errno on failure. + */ +static int sof_static_topology_init_controls(const struct sof_static_topology *topo) +{ + for (size_t i = 0; i < topo->num_controls && i < MAX_STATIC_CONTROLS; i++) { + const struct sof_static_kcontrol *ctl = &topo->controls[i]; + s_control_vals[i] = ctl->def; + int ret = sof_static_kcontrol_set(ctl->id, ctl->def); + if (ret < 0) { + LOG_ERR("Failed to set default value for control [%u] '%s': %d", + ctl->id, ctl->name, ret); + return ret; + } + LOG_INF("Kcontrol [%u] '%s' (comp %u, type %d, def %d)", + ctl->id, ctl->name, ctl->target_comp_id, ctl->type, ctl->def); + } + + return 0; +} + +/** + * \brief Load and instantiate a declarative static audio topology. + * + * Executes the complete multi-stage construction pipeline: + * Stage 1: Allocate top-level pipelines + * Stage 2: Instantiate components (modules, DAIs, USB/host) + * Stage 3: Allocate intermediate audio buffers + * Stage 4: Connect pipeline graph routes + * Stage 5: Bind pipeline endpoints and propagate stream parameters + * Stage 6: Explicitly configure and prepare all components + * Stage 7: Initialize kcontrols with default boot values + * + * \param[in] topo Pointer to declarative static topology descriptor. + * \return 0 on success, negative errno on failure. + */ +/** + * \brief Free all allocated topology resources and reset tracking tables. + * + * Tears down allocated buffers, components, and pipelines in reverse dependency + * order, clears tracking registries, and resets active topology state. + */ +static void sof_static_topology_unwind(void) +{ + /* Free allocated intermediate buffers */ + for (size_t i = 0; i < s_num_buffers; i++) { + if (s_buffers[i]) { + buffer_free(s_buffers[i]); + s_buffers[i] = NULL; + } + } + s_num_buffers = 0; + + /* Free instantiated components */ + for (size_t i = 0; i < s_num_comps; i++) { + if (s_comps[i]) { + comp_free(s_comps[i]); + s_comps[i] = NULL; + } + } + s_num_comps = 0; + + /* Free allocated pipelines */ + for (size_t i = 0; i < s_num_pipelines; i++) { + if (s_pipelines[i]) { + pipeline_free(s_pipelines[i]); + s_pipelines[i] = NULL; + } + } + s_num_pipelines = 0; + + /* Reset kcontrol cache and unpublish active topology pointer */ + memset(s_control_vals, 0, sizeof(s_control_vals)); + s_active_topo = NULL; +} + +int sof_static_topology_init(const struct sof_static_topology *topo) +{ + int ret; + + /* Validate input topology pointer */ + if (!topo) { + LOG_ERR("Invalid topology descriptor"); + return -EINVAL; + } + + /* Validate topology descriptor counts against compiled capacity limits */ + if (topo->num_pipelines > MAX_STATIC_PIPELINES || + topo->num_comps > MAX_STATIC_COMPS || + topo->num_buffers > MAX_STATIC_BUFFERS || + topo->num_controls > MAX_STATIC_CONTROLS || + topo->num_routes > MAX_STATIC_ROUTES) { + LOG_ERR("Topology descriptor counts exceed compiled limits (pipes %zu/%d, comps %zu/%d, bufs %zu/%d, ctls %zu/%d, routes %zu/%d)", + topo->num_pipelines, MAX_STATIC_PIPELINES, + topo->num_comps, MAX_STATIC_COMPS, + topo->num_buffers, MAX_STATIC_BUFFERS, + topo->num_controls, MAX_STATIC_CONTROLS, + topo->num_routes, MAX_STATIC_ROUTES); + return -EINVAL; + } + + LOG_INF("=== Loading Static Audio Topology: '%s' ===", topo->name ? topo->name : "Unnamed"); + + /* Clean up any previously allocated state before constructing new topology */ + sof_static_topology_unwind(); + + /* Stage 1: Allocate top-level pipelines */ + ret = sof_static_topology_init_pipelines(topo); + if (ret < 0) { + LOG_ERR("sof_static_topology_init: stage 1 (init_pipelines) failed: %d", ret); + goto err_unwind; + } + + /* Stage 2: Instantiate components */ + ret = sof_static_topology_init_comps(topo); + if (ret < 0) { + LOG_ERR("sof_static_topology_init: stage 2 (init_comps) failed: %d", ret); + goto err_unwind; + } + + /* Stage 3: Allocate intermediate audio buffers */ + ret = sof_static_topology_init_buffers(topo); + if (ret < 0) { + LOG_ERR("sof_static_topology_init: stage 3 (init_buffers) failed: %d", ret); + goto err_unwind; + } + + /* Stage 4: Connect pipeline graph routes */ + ret = sof_static_topology_init_routes(topo); + if (ret < 0) { + LOG_ERR("sof_static_topology_init: stage 4 (init_routes) failed: %d", ret); + goto err_unwind; + } + + /* Stage 5: Bind pipeline endpoints and propagate stream parameters */ + ret = sof_static_topology_init_pipeline_endpoints(topo); + if (ret < 0) { + LOG_ERR("sof_static_topology_init: stage 5 (init_pipeline_endpoints) failed: %d", ret); + goto err_unwind; + } + + /* Stage 6: Explicitly configure and prepare all components */ + ret = sof_static_topology_prepare_comps(topo); + if (ret < 0) { + LOG_ERR("sof_static_topology_init: stage 6 (prepare_comps) failed: %d", ret); + goto err_unwind; + } + + /* Stage 7: Initialize kcontrols with default values */ + ret = sof_static_topology_init_controls(topo); + if (ret < 0) { + LOG_ERR("sof_static_topology_init: stage 7 (init_controls) failed: %d", ret); + goto err_unwind; + } + + /* Publish topology as active only after all construction stages succeed */ + s_active_topo = topo; + + LOG_INF("Static audio topology initialized successfully (%zu pipelines, %zu comps, %zu buffers, %zu controls)", + topo->num_pipelines, topo->num_comps, topo->num_buffers, topo->num_controls); + return 0; + +err_unwind: + sof_static_topology_unwind(); + return ret; +} + +/** + * \brief Retrieve the currently active static topology descriptor. + * \return Pointer to active struct sof_static_topology, or NULL if uninitialized. + */ +const struct sof_static_topology *sof_static_topology_get(void) +{ + return s_active_topo; +} + +/** + * \brief Retrieve a pipeline instance pointer by its unique pipeline ID. + * \param[in] pipeline_id Unique pipeline ID to search for. + * \return Pointer to struct pipeline, or NULL if not found. + */ +struct pipeline *sof_static_pipeline_get(uint32_t pipeline_id) +{ + /* Linear scan of statically tracked pipeline IDs */ + for (size_t i = 0; i < s_num_pipelines; i++) { + if (s_pipeline_ids[i] == pipeline_id) + return s_pipelines[i]; + } + + LOG_ERR("sof_static_pipeline_get: pipeline %u not found", pipeline_id); + return NULL; +} + +/** + * \brief Retrieve a component device pointer by its unique component ID. + * \param[in] comp_id Unique component ID to search for. + * \return Pointer to struct comp_dev, or NULL if not found. + */ +struct comp_dev *sof_static_comp_get(uint32_t comp_id) +{ + /* Linear scan of statically tracked component IDs */ + for (size_t i = 0; i < s_num_comps; i++) { + if (s_comp_ids[i] == comp_id) + return s_comps[i]; + } + + LOG_ERR("sof_static_comp_get: comp %u not found", comp_id); + return NULL; +} + +/** + * \brief Retrieve an intermediate audio buffer pointer by its unique buffer ID. + * \param[in] buffer_id Unique buffer ID to search for. + * \return Pointer to struct comp_buffer, or NULL if not found. + */ +struct comp_buffer *sof_static_buffer_get(uint32_t buffer_id) +{ + /* Linear scan of statically tracked buffer IDs */ + for (size_t i = 0; i < s_num_buffers; i++) { + if (s_buffer_ids[i] == buffer_id) + return s_buffers[i]; + } + + LOG_ERR("sof_static_buffer_get: buffer %u not found", buffer_id); + return NULL; +} + +/** + * \brief Set kcontrol value by control ID. + * + * Dispatches control updates to the custom callback handler if registered, + * or routes volume, switch, or enum controls to target components via their + * registered static module operations (struct sof_static_module_ops). + * + * \param[in] ctrl_id Unique control identifier. + * \param[in] val Value to apply. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_set(uint32_t ctrl_id, int32_t val) +{ + if (!s_active_topo) + return -ENODEV; + + /* Look up control descriptor by unique ID */ + const struct sof_static_kcontrol *ctl = NULL; + size_t ctl_idx = 0; + for (size_t i = 0; i < s_active_topo->num_controls; i++) { + if (s_active_topo->controls[i].id == ctrl_id) { + ctl = &s_active_topo->controls[i]; + ctl_idx = i; + break; + } + } + if (!ctl) { + LOG_ERR("sof_static_kcontrol_set: control ID %u not found", ctrl_id); + return -ENOENT; + } + + /* + * Custom kcontrol callback handler: + * Allows board- or platform-specific controls (such as hardware clock + * switching, route multiplexers, or DMIC injection) to intercept control events. + */ + if (s_active_topo->custom_control_handler) { + int ret = s_active_topo->custom_control_handler(ctl, val, s_active_topo->custom_control_data); + if (ret >= 0 || ctl->target_comp_id == 0) { + s_control_vals[ctl_idx] = val; + LOG_INF("Kcontrol [%u] '%s' set to %d via custom handler", ctl->id, ctl->name, val); + return ret; + } + } + + /* Virtual / platform controls with no target component */ + if (ctl->target_comp_id == 0) { + s_control_vals[ctl_idx] = val; + LOG_INF("Kcontrol [%u] '%s' stored val=%d", ctl->id, ctl->name, val); + return 0; + } + + /* Look up target component device */ + struct comp_dev *dev = sof_static_comp_get(ctl->target_comp_id); + if (!dev) + return -ENODEV; + + /* Binary controls cannot be updated with a scalar int32_t value */ + if (ctl->type == SOF_STATIC_CTRL_BINARY) { + LOG_ERR("sof_static_kcontrol_set: ctl [%u] '%s' is binary, use sof_static_kcontrol_set_binary", + ctl->id, ctl->name); + return -EINVAL; + } + + /* Look up component's registered static module operations */ + const struct sof_static_module_ops *ops = dev->drv ? sof_static_find_module_ops(dev->drv->uid) : NULL; + if (!ops) { + LOG_ERR("sof_static_kcontrol_set: no static ops for comp %u (ctl [%u] '%s')", + ctl->target_comp_id, ctl->id, ctl->name); + return -ENOTSUP; + } + + int ret_op = -ENOTSUP; + + /* Dispatch control value to target component using registered operations */ + switch (ctl->type) { + case SOF_STATIC_CTRL_VOLUME: + /* Linear volume fader control */ + if (ops->apply_volume) + ret_op = ops->apply_volume(dev, ctl->channels, val); + break; + + case SOF_STATIC_CTRL_SWITCH: + /* Boolean mute or module bypass switch */ + if (ops->apply_switch) + ret_op = ops->apply_switch(dev, ctl->channels, val); + break; + + case SOF_STATIC_CTRL_ENUM: + /* Enumerated route or channel selector */ + if (ops->apply_enum) + ret_op = ops->apply_enum(dev, 0, val); + break; + + default: + LOG_ERR("sof_static_kcontrol_set: invalid control type %d for ctl %u", + ctl->type, ctl->id); + return -EINVAL; + } + + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set: op failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + + /* Cache updated control value and log confirmation */ + s_control_vals[ctl_idx] = val; + LOG_INF("Kcontrol [%u] '%s' set to %d", ctl->id, ctl->name, val); + return 0; +} + +/** + * \brief Set kcontrol value for a specific audio channel. + * \param[in] ctrl_id Unique control identifier. + * \param[in] channel Channel index (0-based). + * \param[in] val Value to apply. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_set_chan(uint32_t ctrl_id, uint32_t channel, int32_t val) +{ + /* Ensure an active static topology has been initialized */ + if (!s_active_topo) { + LOG_ERR("sof_static_kcontrol_set_chan: no active topology"); + return -ENODEV; + } + + const struct sof_static_kcontrol *ctl = NULL; + size_t ctl_idx = 0; + + /* Find control descriptor by ID */ + for (size_t i = 0; i < s_active_topo->num_controls; i++) { + if (s_active_topo->controls[i].id == ctrl_id) { + ctl = &s_active_topo->controls[i]; + ctl_idx = i; + break; + } + } + + if (!ctl) { + LOG_ERR("sof_static_kcontrol_set_chan: control ID %u not found", ctrl_id); + return -ENOENT; + } + + /* Validate channel index against control channel count */ + if (channel >= ctl->channels) { + LOG_ERR("sof_static_kcontrol_set_chan: channel %u out of range (max %u) for ctl %u", + channel, ctl->channels, ctrl_id); + return -EINVAL; + } + + /* + * Custom kcontrol callback handler: + * Forward to platform-specific hook if registered, or acknowledge virtual controls. + */ + if (s_active_topo->custom_control_handler) { + int ret = s_active_topo->custom_control_handler(ctl, val, s_active_topo->custom_control_data); + if (ret >= 0 || ctl->target_comp_id == 0) { + s_control_vals[ctl_idx] = val; + LOG_INF("Kcontrol [%u] '%s' ch %u set to %d via custom handler", + ctl->id, ctl->name, channel, val); + return ret; + } + } + + /* Virtual / platform controls with no target component */ + if (ctl->target_comp_id == 0) { + s_control_vals[ctl_idx] = val; + LOG_INF("Kcontrol [%u] '%s' ch %u stored val=%d", ctl->id, ctl->name, channel, val); + return 0; + } + + /* Look up target component device */ + struct comp_dev *dev = sof_static_comp_get(ctl->target_comp_id); + if (!dev) { + LOG_ERR("sof_static_kcontrol_set_chan: comp %u not found for ctl %u", + ctl->target_comp_id, ctrl_id); + return -ENODEV; + } + + /* Binary controls cannot be updated with a channel scalar value */ + if (ctl->type == SOF_STATIC_CTRL_BINARY) { + LOG_ERR("sof_static_kcontrol_set_chan: ctl [%u] '%s' is binary, use sof_static_kcontrol_set_binary", + ctl->id, ctl->name); + return -EINVAL; + } + + /* Look up component's registered static module operations */ + const struct sof_static_module_ops *ops = dev->drv ? sof_static_find_module_ops(dev->drv->uid) : NULL; + if (!ops) { + LOG_ERR("sof_static_kcontrol_set_chan: no static ops for comp %u (ctl [%u] '%s')", + ctl->target_comp_id, ctl->id, ctl->name); + return -ENOTSUP; + } + + int ret_op = -ENOTSUP; + + /* Dispatch control value to target component using channel-specific ops if available */ + switch (ctl->type) { + case SOF_STATIC_CTRL_VOLUME: + /* Linear volume: prefer channel-aware callback when provided */ + if (ops->apply_volume_chan) + ret_op = ops->apply_volume_chan(dev, channel, val); + else if (ops->apply_volume) + ret_op = ops->apply_volume(dev, ctl->channels, val); + break; + + case SOF_STATIC_CTRL_SWITCH: + /* Boolean switch / mute: prefer channel-aware callback when provided */ + if (ops->apply_switch_chan) + ret_op = ops->apply_switch_chan(dev, channel, val); + else if (ops->apply_switch) + ret_op = ops->apply_switch(dev, ctl->channels, val); + break; + + case SOF_STATIC_CTRL_ENUM: + /* Channel / stream multiplexer selector */ + if (ops->apply_enum) + ret_op = ops->apply_enum(dev, channel, val); + break; + + default: + LOG_ERR("sof_static_kcontrol_set_chan: invalid control type %d for ctl %u", + ctl->type, ctl->id); + return -EINVAL; + } + + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set_chan: op failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + + /* Cache updated control value and log confirmation */ + s_control_vals[ctl_idx] = val; + LOG_INF("Kcontrol [%u] '%s' ch %u set to %d", ctl->id, ctl->name, channel, val); + return 0; +} + +/** + * \brief Set binary raw configuration blob on a static kcontrol by control ID. + * + * Dispatches the raw binary configuration blob to the target component associated + * with the designated kcontrol via apply_binary or module adapter large config. + * + * \param[in] ctrl_id Target kcontrol identifier. + * \param[in] data Pointer to raw configuration blob. + * \param[in] size Size of data blob in bytes. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_set_binary(uint32_t ctrl_id, const void *data, size_t size) +{ + /* Ensure an active static topology has been initialized */ + if (!s_active_topo) { + LOG_ERR("sof_static_kcontrol_set_binary: no active topology"); + return -ENODEV; + } + if (!data || size == 0) { + LOG_ERR("sof_static_kcontrol_set_binary: invalid NULL or empty data"); + return -EINVAL; + } + + const struct sof_static_kcontrol *ctl = NULL; + for (size_t i = 0; i < s_active_topo->num_controls; i++) { + if (s_active_topo->controls[i].id == ctrl_id) { + ctl = &s_active_topo->controls[i]; + break; + } + } + if (!ctl) { + LOG_ERR("sof_static_kcontrol_set_binary: control ID %u not found", ctrl_id); + return -ENOENT; + } + if (ctl->type != SOF_STATIC_CTRL_BINARY) { + LOG_ERR("sof_static_kcontrol_set_binary: control [%u] '%s' is not binary type", + ctl->id, ctl->name); + return -EINVAL; + } + if (ctl->target_comp_id == 0) { + LOG_INF("Kcontrol [%u] '%s' binary blob (%zu bytes) processed", ctl->id, ctl->name, size); + return 0; + } + + struct comp_dev *dev = sof_static_comp_get(ctl->target_comp_id); + if (!dev) { + LOG_ERR("sof_static_kcontrol_set_binary: target comp %u not found", ctl->target_comp_id); + return -ENODEV; + } + + const struct sof_static_module_ops *ops = dev->drv ? sof_static_find_module_ops(dev->drv->uid) : NULL; + if (ops && ops->apply_binary) { + int ret_op = ops->apply_binary(dev, data, size); + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set_binary: apply_binary failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + LOG_INF("Kcontrol [%u] '%s' binary set (%zu bytes)", ctl->id, ctl->name, size); + return 0; + } + + /* Fallback: dispatch via component driver set_large_config or send_comp_config */ + int ret; + if (dev->drv && dev->drv->ops.set_large_config) + ret = dev->drv->ops.set_large_config(dev, 0, true, true, size, (const char *)data); + else + ret = send_comp_config(dev, data, size); + + if (ret < 0) { + LOG_ERR("sof_static_kcontrol_set_binary: binary set failed %d for ctl [%u] '%s'", + ret, ctl->id, ctl->name); + return ret; + } + + LOG_INF("Kcontrol [%u] '%s' binary set (%zu bytes)", ctl->id, ctl->name, size); + return 0; +} + +/** + * \brief Retrieve cached kcontrol value by control ID. + * \param[in] ctrl_id Unique control identifier. + * \param[out] val Pointer to store retrieved value. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_get(uint32_t ctrl_id, int32_t *val) +{ + if (!s_active_topo || !val) + return -EINVAL; + + /* Search control descriptors for matching ID and return cached value */ + for (size_t i = 0; i < s_active_topo->num_controls; i++) { + if (s_active_topo->controls[i].id == ctrl_id) { + *val = s_control_vals[i]; + return 0; + } + } + + LOG_ERR("sof_static_kcontrol_get: control ID %u not found", ctrl_id); + return -ENOENT; +} + +/** + * \brief Find kcontrol ID by its human-readable name. + * \param[in] name Control name string to search for. + * \return Control ID >= 0 if found, -ENOENT if not found. + */ +int sof_static_kcontrol_find_by_name(const char *name) +{ + if (!s_active_topo || !name) + return -EINVAL; + + /* Compare name string against declared controls in active topology */ + for (size_t i = 0; i < s_active_topo->num_controls; i++) { + if (s_active_topo->controls[i].name && + strcmp(s_active_topo->controls[i].name, name) == 0) { + return (int)s_active_topo->controls[i].id; + } + } + + LOG_ERR("sof_static_kcontrol_find_by_name: control '%s' not found", name); + return -ENOENT; +} + +/** + * \brief Resolve the primary trigger component driving a pipeline. + * + * For playback pipelines, the trigger component is typically sched_comp or source_comp. + * For capture pipelines, where data flows into the DSP from an external DAI or source, + * the trigger component must be the consumer/sink endpoint to pull data through the graph. + * + * \param[in] pipe Target pipeline pointer. + * \return Pointer to resolved trigger component device. + */ +static struct comp_dev *sof_static_pipeline_get_trigger_dev(struct pipeline *pipe) +{ + struct comp_dev *dev = pipe->sched_comp; + + /* For capture pipelines, ensure trigger starts from the sink component */ + if (!dev || (pipe->source_comp && pipe->source_comp->direction == SOF_IPC_STREAM_CAPTURE && + dev == pipe->source_comp)) { + if (pipe->source_comp && pipe->source_comp->direction == SOF_IPC_STREAM_CAPTURE) + dev = pipe->sink_comp; + else + dev = pipe->source_comp; + } + return dev; +} + +/** + * \brief Start a static audio pipeline by its pipeline ID. + * + * Prepares the pipeline and constituent components if needed, propagates + * start triggers, transitions state to COMP_STATE_ACTIVE, and activates + * scheduling copy tasks. + * + * \param[in] pipeline_id Target pipeline ID. + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_start(uint32_t pipeline_id) +{ + /* Look up target pipeline */ + struct pipeline *pipe = sof_static_pipeline_get(pipeline_id); + if (!pipe) + return -ENOENT; + + /* Resolve trigger component */ + struct comp_dev *dev = sof_static_pipeline_get_trigger_dev(pipe); + if (!dev) + return -ENODEV; + + /* If not already active, prepare and trigger components */ + if (pipe->status != COMP_STATE_ACTIVE) { + /* + * Ensure pipeline is prepared before starting. + * If prepare fails, abort immediately: triggering a pipeline whose + * circular buffers and DMA channels are uninitialized causes DSP faults. + */ + if (pipe->status == COMP_STATE_READY || + pipe->status == COMP_STATE_INIT || + pipe->status == COMP_STATE_PAUSED) { + int ret_prep = pipeline_prepare(pipe, dev); + if (ret_prep < 0) { + LOG_ERR("Pipeline %u prepare failed: %d", pipeline_id, ret_prep); + return ret_prep; + } + } + + /* Propagate PRE_START trigger across the pipeline graph (pipeline_trigger_run + * automatically executes the follow-up START traversal on all graph components). + */ + int ret = pipeline_trigger_run(pipe, dev, COMP_TRIGGER_PRE_START); + if (ret < 0) { + LOG_ERR("Pipeline %u pre-start trigger failed: %d", pipeline_id, ret); + return ret; + } + + pipe->status = COMP_STATE_ACTIVE; + } + + /* Schedule periodic copy task if not already running */ + if (pipe->pipe_task && !task_is_active(pipe->pipe_task)) + pipeline_schedule_copy(pipe, 0); + + LOG_INF("Pipeline %u STARTED", pipeline_id); + return 0; +} + +/** + * \brief Stop a static audio pipeline by its pipeline ID. + * + * Propagates stop triggers, cancels scheduling copy tasks, and transitions + * pipeline and component states to COMP_STATE_PAUSED. + * + * \param[in] pipeline_id Target pipeline ID. + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_stop(uint32_t pipeline_id) +{ + /* Look up target pipeline */ + struct pipeline *pipe = sof_static_pipeline_get(pipeline_id); + if (!pipe) + return -ENOENT; + + /* Resolve trigger component */ + struct comp_dev *dev = sof_static_pipeline_get_trigger_dev(pipe); + if (!dev) + return -ENODEV; + + /* Propagate STOP trigger if pipeline is active or paused */ + if (pipe->status == COMP_STATE_ACTIVE || pipe->status == COMP_STATE_PAUSED) { + int ret = pipeline_trigger_run(pipe, dev, COMP_TRIGGER_STOP); + if (ret < 0) { + LOG_ERR("Pipeline %u stop trigger failed: %d", pipeline_id, ret); + return ret; + } + + pipe->status = COMP_STATE_PAUSED; + } + + /* Cancel active periodic copy task */ + if (pipe->pipe_task && task_is_active(pipe->pipe_task)) + schedule_task_cancel(pipe->pipe_task); + + LOG_INF("Pipeline %u STOPPED", pipeline_id); + return 0; +} + +/** + * \brief Directly trigger a static pipeline start or stop by pipeline ID. + * + * Dispatches cleanly to sof_static_pipeline_start() or sof_static_pipeline_stop(). + * + * \param[in] pipeline_id Target pipeline ID. + * \param[in] start True to start pipeline, false to stop. + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_trigger(uint32_t pipeline_id, bool start) +{ + if (start) + return sof_static_pipeline_start(pipeline_id); + + return sof_static_pipeline_stop(pipeline_id); +} + +/** + * \brief Re-negotiate the stream sample rate for a specific static pipeline. + * + * Re-negotiates the sample rate for the designated pipeline and its constituent + * components and buffers while the pipeline is in paused or stopped state. + * Validates that all constituent components support the requested rate according + * to their capability masks, reconfigures buffer period sizes, and propagates + * pipeline_params() and pipeline_prepare() through the pipeline graph. + * + * \param[in] pipeline_id Target pipeline identifier. + * \param[in] rate Target sample rate in Hz (e.g. 16000, 44100, 48000, 96000). + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_set_sample_rate(uint32_t pipeline_id, uint32_t rate) +{ + /* Verify target pipeline exists */ + struct pipeline *pipe = sof_static_pipeline_get(pipeline_id); + if (!pipe) { + LOG_ERR("sof_static_pipeline_set_sample_rate: pipeline %u not found", pipeline_id); + return -ENOENT; + } + + /* Dynamic rate reconfiguration requires the pipeline to be stopped/paused */ + if (pipe->status == COMP_STATE_ACTIVE) { + LOG_ERR("sof_static_pipeline_set_sample_rate: pipeline %u is active, cannot change rate", + pipeline_id); + return -EBUSY; + } + + /* Validate requested sample rate against component capability masks */ + uint32_t rmask = sof_static_rate_to_mask(rate); + if (s_active_topo) { + for (size_t c = 0; c < s_active_topo->num_comps; c++) { + const struct sof_static_comp *cdesc = &s_active_topo->comps[c]; + if (cdesc->pipeline_id == pipeline_id && cdesc->caps.rates) { + if (!(cdesc->caps.rates & rmask)) { + LOG_ERR("sof_static_pipeline_set_sample_rate: rate %u Hz unsupported by comp %u ('%s')", + rate, cdesc->id, cdesc->name); + return -EINVAL; + } + } + } + } + + /* Record updated sample rate */ + for (size_t i = 0; i < s_num_pipelines; i++) { + if (s_pipeline_ids[i] == pipeline_id) { + s_pipeline_rates[i] = rate; + break; + } + } + + /* Update buffer parameters for all buffers whose producer belongs to this pipeline */ + if (s_active_topo) { + for (size_t r = 0; r < s_active_topo->num_routes; r++) { + const struct sof_static_route *route = &s_active_topo->routes[r]; + const struct sof_static_comp *src_cdesc = + sof_static_find_comp_desc(s_active_topo, route->src_comp_id); + + if (src_cdesc && src_cdesc->pipeline_id == pipeline_id) { + struct comp_buffer *buf = sof_static_buffer_get(route->buffer_id); + + if (buf) { + init_buffer_params(buf, src_cdesc->direction, + audio_stream_get_frm_fmt(&buf->stream), + rate, + audio_stream_get_channels(&buf->stream)); + } + } + } + } + + /* Propagate updated stream parameters through pipeline graph */ + struct comp_dev *host_or_sched = pipe->sched_comp ? pipe->sched_comp : pipe->source_comp; + if (host_or_sched) { + const struct sof_static_pipeline_desc *pdesc = NULL; + if (s_active_topo) + pdesc = sof_static_find_pipeline_desc(s_active_topo, pipeline_id); + + uint32_t channels = 2; + enum sof_ipc_frame fmt = host_or_sched->ipc_config.frame_fmt; + if (!fmt) + fmt = SOF_IPC_FRAME_S16_LE; + + uint32_t cont_bytes = get_sample_bytes(fmt); + uint32_t valid_bytes = static_get_sample_valid_bytes(fmt); + uint32_t period_us = (pdesc && pdesc->period) ? pdesc->period : 1000; + uint32_t frames = (rate * period_us) / 1000000; + if (frames == 0) + frames = 48; + + struct sof_ipc_pcm_params prms; + memset(&prms, 0, sizeof(prms)); + prms.params.rate = rate; + prms.params.channels = channels; + prms.params.frame_fmt = fmt; + prms.params.sample_container_bytes = cont_bytes; + prms.params.sample_valid_bytes = valid_bytes; + prms.params.buffer_fmt = SOF_IPC_BUFFER_INTERLEAVED; + prms.params.host_period_bytes = frames * channels * cont_bytes; + prms.comp_id = dev_comp_id(host_or_sched); + prms.params.direction = host_or_sched->direction; + for (uint32_t c = 0; c < SOF_IPC_MAX_CHANNELS; c++) { + if (c == 0) + prms.params.chmap[c] = (channels == 1) ? SOF_CHMAP_MONO : SOF_CHMAP_FL; + else if (c == 1 && channels > 1) + prms.params.chmap[c] = SOF_CHMAP_FR; + else + prms.params.chmap[c] = SOF_CHMAP_NA; + } + + int ret_params = pipeline_params(pipe, host_or_sched, &prms); + if (ret_params < 0) { + LOG_ERR("sof_static_pipeline_set_sample_rate: pipeline_params failed %d for pipeline %u", + ret_params, pipeline_id); + return ret_params; + } + + int ret_prep = pipeline_prepare(pipe, host_or_sched); + if (ret_prep < 0) { + LOG_ERR("sof_static_pipeline_set_sample_rate: pipeline_prepare failed %d for pipeline %u", + ret_prep, pipeline_id); + return ret_prep; + } + } + + LOG_INF("Static pipeline %u sample rate reconfigured to %u Hz", pipeline_id, rate); + return 0; +} + +/** + * \brief Retrieve the current stream sample rate for a specific static pipeline. + * \param[in] pipeline_id Target pipeline identifier. + * \return Pipeline sample rate in Hz, or 0 if pipeline not found. + */ +uint32_t sof_static_pipeline_get_sample_rate(uint32_t pipeline_id) +{ + for (size_t i = 0; i < s_num_pipelines; i++) { + if (s_pipeline_ids[i] == pipeline_id) + return s_pipeline_rates[i] ? s_pipeline_rates[i] : 48000; + } + + LOG_ERR("sof_static_pipeline_get_sample_rate: pipeline %u not found", pipeline_id); + return 0; +} + +/** + * \brief Override the stream sample rate across all active static pipelines. + * \param[in] rate Target sample rate in Hz (e.g. 48000). + * \return 0 on success, negative errno on failure. + */ +int sof_static_set_sample_rate(uint32_t rate) +{ + if (s_num_pipelines == 0) { + LOG_ERR("sof_static_set_sample_rate: no active static pipelines"); + return -ENODEV; + } + + int last_ret = 0; + for (size_t i = 0; i < s_num_pipelines; i++) { + int ret = sof_static_pipeline_set_sample_rate(s_pipeline_ids[i], rate); + if (ret < 0) + last_ret = ret; + } + + LOG_INF("All static pipelines sample rate set to %u Hz", rate); + return last_ret; +} + +/** + * \brief Retrieve the primary stream sample rate from the first static pipeline. + * \return Current sample rate in Hz. + */ +uint32_t sof_static_get_sample_rate(void) +{ + if (s_num_pipelines > 0 && s_pipeline_rates[0]) + return s_pipeline_rates[0]; + + return 48000; +} From d53111c554602484ecb3a464ef117e53f680b98f Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 11 Oct 2026 13:45:41 +0100 Subject: [PATCH 5/7] audio: pipeline: add USB Audio Class 2 (UAC2) static bridge and control operations Add static_pipeline_uac2.c providing translation between USB Audio Class 2.0 (UAC2) requests (8.8 dB volume representation, mute switches, streaming terminal trigger events) and SOF static pipelines and kcontrols. Signed-off-by: Liam Girdwood --- src/audio/pipeline/CMakeLists.txt | 1 + src/audio/pipeline/static_pipeline_uac2.c | 309 ++++++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 src/audio/pipeline/static_pipeline_uac2.c diff --git a/src/audio/pipeline/CMakeLists.txt b/src/audio/pipeline/CMakeLists.txt index c812ce02d04a..eca2f06857d0 100644 --- a/src/audio/pipeline/CMakeLists.txt +++ b/src/audio/pipeline/CMakeLists.txt @@ -11,4 +11,5 @@ add_local_sources(sof add_local_sources_ifdef(CONFIG_STATIC_PIPELINE sof static_pipeline_loader.c static_pipeline_modules.c + static_pipeline_uac2.c ) diff --git a/src/audio/pipeline/static_pipeline_uac2.c b/src/audio/pipeline/static_pipeline_uac2.c new file mode 100644 index 000000000000..7b4506d9289c --- /dev/null +++ b/src/audio/pipeline/static_pipeline_uac2.c @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Sound Open Firmware (SOF) Project +// +// Author: Liam Girdwood + +/** + * \file audio/pipeline/static_pipeline_uac2.c + * \brief USB Audio Class 2.0 (UAC2) Bridge & Control Operations for Static Pipelines + * \author Liam Girdwood + * + * Provides translation between USB Audio Class 2.0 (UAC2) host requests (volume, + * mute switch, streaming terminal triggers) and Sound Open Firmware static + * topology components and kcontrols. + */ + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(static_pipeline_uac2, CONFIG_SOF_LOG_LEVEL); + +/* Standard 0 dB gain constant for SOF volume representation (Q1.31 format) */ +#define SOF_VOL_ZERO_DB INT32_MAX + +#ifndef CONFIG_STATIC_PIPELINE_MAX_CONTROLS +#define CONFIG_STATIC_PIPELINE_MAX_CONTROLS 64 +#endif + +/* + * Cache structure storing raw UAC2 control values (signed 8.8 dB for volume, + * 0/1 for mute) per UAC2 entity and channel. + * Channel index 0 corresponds to Master; indices 1..SOF_IPC_MAX_CHANNELS correspond + * to individual audio channels. + */ +struct uac2_control_cache { + uint8_t entity_id; + bool is_volume; + bool valid; + int32_t val[SOF_IPC_MAX_CHANNELS + 1]; +}; + +static APP_SYSUSER_BSS struct uac2_control_cache s_uac2_cache[CONFIG_STATIC_PIPELINE_MAX_CONTROLS]; + +/** + * \brief Look up or allocate a cached control state entry for a UAC2 entity. + * \param[in] entity_id UAC2 Feature Unit Entity ID. + * \param[in] is_volume True for volume control, false for mute switch. + * \return Pointer to cache entry or NULL if table is full. + */ +static struct uac2_control_cache *get_uac2_cache(uint8_t entity_id, bool is_volume) +{ + /* Look for existing cache entry matching entity and control type */ + for (size_t i = 0; i < CONFIG_STATIC_PIPELINE_MAX_CONTROLS; i++) { + if (s_uac2_cache[i].valid && + s_uac2_cache[i].entity_id == entity_id && + s_uac2_cache[i].is_volume == is_volume) + return &s_uac2_cache[i]; + } + + /* Allocate a free cache slot if not already present */ + for (size_t i = 0; i < CONFIG_STATIC_PIPELINE_MAX_CONTROLS; i++) { + if (!s_uac2_cache[i].valid) { + s_uac2_cache[i].entity_id = entity_id; + s_uac2_cache[i].is_volume = is_volume; + s_uac2_cache[i].valid = true; + /* Default: volume 0 dB = 0x0000, mute = 0 (unmuted) */ + memset(s_uac2_cache[i].val, 0, sizeof(s_uac2_cache[i].val)); + return &s_uac2_cache[i]; + } + } + + return NULL; +} + +/** + * \brief Convert USB Audio Class 2.0 8.8 fixed-point dB volume to linear SOF volume. + * + * UAC2 expresses volume in 1/256 dB steps (e.g. 0x0000 = 0 dB, -90 dB = 0xa600). + * This helper translates UAC2 dB volume into SOF 32-bit linear volume using + * an integer 6 dB per bit shift approximation: + * - >= 0 dB maps to SOF_VOL_ZERO_DB (INT32_MAX, unity gain) + * - <= -90 dB maps to 0 (digital silence) + * - Values between -90 dB and 0 dB are scaled proportionally. + * + * \param[in] volume USB Audio Class 2.0 8.8 fixed-point dB volume value. + * \return 32-bit linear volume value (0 to SOF_VOL_ZERO_DB). + */ +static int32_t uac2_to_sof_volume(int16_t volume) +{ + /* Treat volume below -90 dB as complete digital silence */ + if (volume <= -90 * 256) + return 0; + + /* Clip volume at or above 0 dB to unity gain */ + if (volume >= 0) + return SOF_VOL_ZERO_DB; + + /* Convert negative dB (scaled by 256) to tenths of dB: 1 dB = ~6 dB per bit */ + int32_t db_x10 = (int32_t)(-volume) * 10 / 256; + int shift = db_x10 / 60; + if (shift >= 31) + return 0; + + /* + * Monotonically interpolate between 6 dB bit shifts. + * v is the gain at the current 6 dB boundary, and v_next is the gain at the + * next 6 dB boundary (v >> 1). We interpolate smoothly between v and v_next. + */ + int rem = db_x10 % 60; + uint64_t v = (uint64_t)SOF_VOL_ZERO_DB >> shift; + uint64_t v_next = v >> 1; + uint64_t val = v_next + (((v - v_next) * (60 - rem)) / 60); + + return (int32_t)val; +} + +/** + * \brief Dispatch USB Audio Class 2.0 Feature Unit control requests to matching kcontrols. + * + * Maps incoming USB Audio Class 2 (UAC2) volume or mute requests to the corresponding + * static kcontrol bound to that UAC2 entity ID, supporting channel 0 (master) and + * individual channels. + * + * \param[in] entity_id UAC2 Feature Unit Entity ID. + * \param[in] channel Audio channel index (0 = master/all, 1 = first channel, etc.). + * \param[in] val Value from USB request (8.8 fixed-point dB volume or boolean mute). + * \param[in] is_volume True for volume command, false for mute switch command. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_set_by_uac2(uint8_t entity_id, uint8_t channel, int32_t val, bool is_volume) +{ + const struct sof_static_topology *topo = sof_static_topology_get(); + + if (!topo) { + LOG_ERR("sof_static_kcontrol_set_by_uac2: no active topology"); + return -ENODEV; + } + + /* Search controls in active topology for matching UAC2 entity ID */ + for (size_t i = 0; i < topo->num_controls; i++) { + const struct sof_static_kcontrol *ctl = &topo->controls[i]; + + if (ctl->uac2_entity_id == entity_id) { + /* Validate channel bounds */ + if (channel > ctl->channels && channel != 0) { + LOG_ERR("sof_static_kcontrol_set_by_uac2: channel %u exceeds ctl channels %u", + channel, ctl->channels); + return -EINVAL; + } + + struct uac2_control_cache *cache = get_uac2_cache(entity_id, is_volume); + + if (is_volume && ctl->type == SOF_STATIC_CTRL_VOLUME) { + /* Translate UAC2 8.8 dB volume to linear SOF Q1.31 volume */ + int32_t sof_vol = uac2_to_sof_volume((int16_t)val); + int ret; + + if (channel == 0) { + /* Master channel: apply volume change to all channels first */ + ret = sof_static_kcontrol_set(ctl->id, sof_vol); + if (ret < 0) + return ret; + + /* Update cache only after the hardware/DSP operation succeeds */ + if (cache) { + for (uint32_t ch = 0; ch <= ctl->channels && ch <= SOF_IPC_MAX_CHANNELS; ch++) + cache->val[ch] = val; + } + return 0; + } + + /* Individual channel: apply volume change to specific channel first */ + ret = sof_static_kcontrol_set_chan(ctl->id, channel - 1, sof_vol); + if (ret < 0) + return ret; + + /* Update channel cache only after the hardware/DSP operation succeeds */ + if (cache && channel <= SOF_IPC_MAX_CHANNELS) + cache->val[channel] = val; + + return 0; + } else if (!is_volume && ctl->type == SOF_STATIC_CTRL_SWITCH) { + /* In UAC2: val=1 means MUTED, so enabled switch state is 0 */ + int32_t sof_sw = val ? 0 : 1; + int ret; + + if (channel == 0) { + /* Master channel: apply mute switch change to all channels first */ + ret = sof_static_kcontrol_set(ctl->id, sof_sw); + if (ret < 0) + return ret; + + /* Update cache only after the hardware/DSP operation succeeds */ + if (cache) { + for (uint32_t ch = 0; ch <= ctl->channels && ch <= SOF_IPC_MAX_CHANNELS; ch++) + cache->val[ch] = val; + } + return 0; + } + + /* Individual channel: apply mute switch change to specific channel first */ + ret = sof_static_kcontrol_set_chan(ctl->id, channel - 1, sof_sw); + if (ret < 0) + return ret; + + /* Update channel cache only after the hardware/DSP operation succeeds */ + if (cache && channel <= SOF_IPC_MAX_CHANNELS) + cache->val[channel] = val; + + return 0; + } + } + } + + LOG_ERR("sof_static_kcontrol_set_by_uac2: no control found for UAC2 entity %u", entity_id); + return -ENOENT; +} + +/** + * \brief Query kcontrol value formatted for USB Audio Class 2.0 response. + * + * \param[in] entity_id UAC2 Feature Unit Entity ID. + * \param[in] channel Audio channel index (0 = master, 1 = first channel, etc.). + * \param[out] val Pointer to store retrieved UAC2 value. + * \param[in] is_volume True for volume query, false for mute query. + * \return 0 on success, negative errno on failure. + */ +int sof_static_kcontrol_get_by_uac2(uint8_t entity_id, uint8_t channel, int32_t *val, bool is_volume) +{ + const struct sof_static_topology *topo = sof_static_topology_get(); + + if (!topo || !val) { + LOG_ERR("sof_static_kcontrol_get_by_uac2: invalid topology or null val pointer"); + return -EINVAL; + } + + /* Search controls in active topology for matching UAC2 entity ID */ + for (size_t i = 0; i < topo->num_controls; i++) { + const struct sof_static_kcontrol *ctl = &topo->controls[i]; + + if (ctl->uac2_entity_id == entity_id) { + /* Validate channel bounds */ + if (channel > ctl->channels && channel != 0) { + LOG_ERR("sof_static_kcontrol_get_by_uac2: channel %u exceeds ctl channels %u", + channel, ctl->channels); + return -EINVAL; + } + + /* Retrieve cached UAC2 value (exact 8.8 dB or mute state) */ + struct uac2_control_cache *cache = get_uac2_cache(entity_id, is_volume); + if (cache && channel <= SOF_IPC_MAX_CHANNELS) { + *val = cache->val[channel]; + return 0; + } + + /* Default if uninitialized: 0 dB for volume, 0 (unmuted) for switch */ + *val = 0; + return 0; + } + } + + LOG_ERR("sof_static_kcontrol_get_by_uac2: no control found for UAC2 entity %u", entity_id); + return -ENOENT; +} + +/** + * \brief Trigger pipeline start or stop associated with a UAC2 terminal ID. + * + * Finds the host streaming component bound to the given USB terminal ID + * and delegates to sof_static_pipeline_start() or sof_static_pipeline_stop(). + * + * \param[in] terminal_id Bound UAC2 Terminal Entity ID. + * \param[in] start True to start pipeline, false to pause/stop. + * \return 0 on success, negative errno on failure. + */ +int sof_static_pipeline_trigger_by_uac2_term(uint8_t terminal_id, bool start) +{ + const struct sof_static_topology *topo = sof_static_topology_get(); + + if (!topo) { + LOG_ERR("sof_static_pipeline_trigger_by_uac2_term: no active topology"); + return -ENODEV; + } + + /* Find host component matching terminal_id */ + for (size_t i = 0; i < topo->num_comps; i++) { + const struct sof_static_comp *cdesc = &topo->comps[i]; + + if (cdesc->type == SOF_STATIC_COMP_HOST && cdesc->ep.usb.terminal_id == terminal_id) { + /* Delegate directly to modular start/stop routines */ + int ret = start ? sof_static_pipeline_start(cdesc->pipeline_id) : + sof_static_pipeline_stop(cdesc->pipeline_id); + + if (ret == 0) + LOG_INF("Pipeline %u %s via UAC2 terminal %u", cdesc->pipeline_id, + start ? "STARTED" : "STOPPED", terminal_id); + else + LOG_ERR("Pipeline %u %s via UAC2 terminal %u failed: %d", + cdesc->pipeline_id, start ? "start" : "stop", terminal_id, ret); + + return ret; + } + } + + LOG_ERR("sof_static_pipeline_trigger_by_uac2_term: no host component found for terminal %u", terminal_id); + return -ENOENT; +} From 38ecd2d873bdf661dddb9afcf4c780a692f6438c Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 11 Oct 2026 13:45:44 +0100 Subject: [PATCH 6/7] audio: modules: add static pipeline module operations Register static module operations (struct sof_static_module_ops) across core audio processing components (Volume, Gain, Level Multiplier, SRC, ASRC, Selector, EQ IIR, DRC, and TDFB) for IPC4 static pipeline execution. Includes explicit error logging on invalid module pointers or private data. Signed-off-by: Liam Girdwood --- src/audio/asrc/asrc.c | 44 +++++ src/audio/drc/drc.c | 33 ++++ src/audio/eq_iir/eq_iir.c | 87 +++++++++ src/audio/level_multiplier/level_multiplier.c | 67 +++++++ src/audio/selector/selector.c | 150 ++++++++++++++++ src/audio/src/src.c | 31 ++++ src/audio/tdfb/tdfb.c | 36 ++++ src/audio/volume/volume.c | 166 ++++++++++++++++++ 8 files changed, 614 insertions(+) diff --git a/src/audio/asrc/asrc.c b/src/audio/asrc/asrc.c index f75009ffd775..69909fc064f7 100644 --- a/src/audio/asrc/asrc.c +++ b/src/audio/asrc/asrc.c @@ -846,4 +846,48 @@ SOF_LLEXT_BUILDINFO; DECLARE_MODULE_ADAPTER(asrc_interface, ASRC_UUID, asrc_tr); SOF_MODULE_INIT(asrc, sys_comp_module_asrc_interface_init); +#if CONFIG_STATIC_PIPELINE +#include + +#if CONFIG_IPC_MAJOR_4 +#include "asrc_ipc4.h" + +static struct comp_dev *asrc_static_create(const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us) +{ + struct ipc4_asrc_module_cfg asrc_cfg; + + memset(&asrc_cfg, 0, sizeof(asrc_cfg)); + sof_static_init_base_cfg(&asrc_cfg.base, cdesc, period_us); + asrc_cfg.out_freq = (enum ipc4_sampling_frequency)(cdesc->caps.sink_rate ? + cdesc->caps.sink_rate : + cdesc->caps.default_rate); + + /* + * Configure ASRC mode bitmask according to stream direction: + * Playback streams operate in PUSH mode (bit 0), while capture streams + * operate in PULL mode (bit 1). This ensures correct endpoint searching + * and buffer size calculation in the ASRC module. + */ + asrc_cfg.asrc_mode = (cdesc->direction == SOF_IPC_STREAM_PLAYBACK) ? + BIT(IPC4_MOD_ASRC_PUSH_MODE) : BIT(IPC4_MOD_ASRC_PULL_MODE); + + struct ipc_config_process spec = { + .size = sizeof(asrc_cfg), + .data = (const uint8_t *)&asrc_cfg, + }; + return drv->ops.create(drv, cfg, &spec); +} + +static struct sof_static_module_ops asrc_static_ops = { + .uuid = &ASRC_UUID, + .create = asrc_static_create, +}; + +DECLARE_STATIC_MODULE_OPS(asrc, &asrc_static_ops); +#endif /* CONFIG_IPC_MAJOR_4 */ +#endif /* CONFIG_STATIC_PIPELINE */ + #endif diff --git a/src/audio/drc/drc.c b/src/audio/drc/drc.c index 8709d9abcb4b..ceb1b1b0bd6e 100644 --- a/src/audio/drc/drc.c +++ b/src/audio/drc/drc.c @@ -481,4 +481,37 @@ SOF_LLEXT_BUILDINFO; DECLARE_MODULE_ADAPTER(drc_interface, drc_uuid, drc_tr); SOF_MODULE_INIT(drc, sys_comp_module_drc_interface_init); +#if CONFIG_STATIC_PIPELINE +#include + +static int drc_static_apply_switch(struct comp_dev *dev, uint32_t channels, int32_t val) +{ + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "drc_static_apply_switch: NULL processing module"); + return -EINVAL; + } + + struct drc_comp_data *cd = module_get_private_data(mod); + + if (!cd) { + comp_err(dev, "drc_static_apply_switch: NULL private data"); + return -EINVAL; + } + + /* Update switch state and recompute active enabled state immediately */ + cd->enable_switch = (val != 0); + cd->enabled = cd->config && cd->config->params.enabled && cd->enable_switch; + return 0; +} + +static struct sof_static_module_ops drc_static_ops = { + .uuid = &drc_uuid, + .apply_switch = drc_static_apply_switch, +}; + +DECLARE_STATIC_MODULE_OPS(drc, &drc_static_ops); +#endif /* CONFIG_STATIC_PIPELINE */ + #endif diff --git a/src/audio/eq_iir/eq_iir.c b/src/audio/eq_iir/eq_iir.c index a9a4bc4aa24a..5451c88e2ad3 100644 --- a/src/audio/eq_iir/eq_iir.c +++ b/src/audio/eq_iir/eq_iir.c @@ -258,4 +258,91 @@ DECLARE_TR_CTX(eq_iir_tr, SOF_UUID(eq_iir_uuid), LOG_LEVEL_INFO); DECLARE_MODULE_ADAPTER(eq_iir_interface, eq_iir_uuid, eq_iir_tr); SOF_MODULE_INIT(eq_iir, sys_comp_module_eq_iir_interface_init); +#if CONFIG_STATIC_PIPELINE +#include + +static int eq_iir_static_apply_switch(struct comp_dev *dev, uint32_t channels, int32_t val) +{ + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "eq_iir_static_apply_switch: NULL processing module"); + return -EINVAL; + } + + struct comp_data *cd = module_get_private_data(mod); + + if (!cd) { + comp_err(dev, "eq_iir_static_apply_switch: NULL private data"); + return -EINVAL; + } + + if (val == 0) { + /* Disable EQ by engaging pass-through routine */ + cd->eq_iir_func = eq_iir_pass; + } else if (cd->iir_delay_size) { + /* Re-enable EQ processing matching configured valid bit depth */ + unsigned int valid_bit_depth = mod->priv.cfg.base_cfg.audio_fmt.valid_bit_depth; + + switch (valid_bit_depth) { +#if CONFIG_FORMAT_S16LE + case IPC4_DEPTH_16BIT: + cd->eq_iir_func = eq_iir_s16_default; + break; +#endif +#if CONFIG_FORMAT_S24LE + case IPC4_DEPTH_24BIT: + cd->eq_iir_func = eq_iir_s24_default; + break; +#endif +#if CONFIG_FORMAT_S32LE + case IPC4_DEPTH_32BIT: + cd->eq_iir_func = eq_iir_s32_default; + break; +#endif + default: + /* Fallback inspecting producer stream frame format */ + { + struct comp_buffer *sourceb = comp_dev_get_first_data_producer(dev); + enum sof_ipc_frame fmt = sourceb ? + audio_stream_get_frm_fmt(&sourceb->stream) : + SOF_IPC_FRAME_S32_LE; + + switch (fmt) { +#if CONFIG_FORMAT_S16LE + case SOF_IPC_FRAME_S16_LE: + cd->eq_iir_func = eq_iir_s16_default; + break; +#endif +#if CONFIG_FORMAT_S24LE + case SOF_IPC_FRAME_S24_4LE: + case SOF_IPC_FRAME_S24_3LE: + cd->eq_iir_func = eq_iir_s24_default; + break; +#endif + default: +#if CONFIG_FORMAT_S32LE + cd->eq_iir_func = eq_iir_s32_default; +#elif CONFIG_FORMAT_S16LE + cd->eq_iir_func = eq_iir_s16_default; +#else + cd->eq_iir_func = eq_iir_pass; +#endif + break; + } + } + break; + } + } + return 0; +} + +static struct sof_static_module_ops eq_iir_static_ops = { + .uuid = &eq_iir_uuid, + .apply_switch = eq_iir_static_apply_switch, +}; + +DECLARE_STATIC_MODULE_OPS(eq_iir, &eq_iir_static_ops); +#endif /* CONFIG_STATIC_PIPELINE */ + #endif diff --git a/src/audio/level_multiplier/level_multiplier.c b/src/audio/level_multiplier/level_multiplier.c index 4fca482ced72..13945c622898 100644 --- a/src/audio/level_multiplier/level_multiplier.c +++ b/src/audio/level_multiplier/level_multiplier.c @@ -204,4 +204,71 @@ DECLARE_TR_CTX(level_multiplier_tr, SOF_UUID(level_multiplier_uuid), LOG_LEVEL_I DECLARE_MODULE_ADAPTER(level_multiplier_interface, level_multiplier_uuid, level_multiplier_tr); SOF_MODULE_INIT(level_multiplier, sys_comp_module_level_multiplier_interface_init); +#if CONFIG_STATIC_PIPELINE +#include + +static int lm_static_apply_volume(struct comp_dev *dev, uint32_t channels, int32_t val) +{ + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "lm_static_apply_volume: NULL processing module"); + return -EINVAL; + } + + struct level_multiplier_comp_data *cd = module_get_private_data(mod); + + if (!cd) { + comp_err(dev, "lm_static_apply_volume: NULL private data"); + return -EINVAL; + } + + /* + * Convert incoming Q1.31 volume (where INT32_MAX represents unity gain 0 dB) + * to the internal Q9.23 format (where LEVEL_MULTIPLIER_GAIN_ONE = 1 << 23). + * We add rounding (+ 128) before shifting right by 8 bits, and clamp + * non-positive values to 0 and values above unity to LEVEL_MULTIPLIER_GAIN_ONE. + */ + if (val <= 0) { + cd->gain = 0; + } else { + int64_t gain_q23 = ((int64_t)val + (1 << 7)) >> 8; + + if (gain_q23 > LEVEL_MULTIPLIER_GAIN_ONE) + gain_q23 = LEVEL_MULTIPLIER_GAIN_ONE; + + cd->gain = (int32_t)gain_q23; + } + return 0; +} + +static int lm_static_apply_switch(struct comp_dev *dev, uint32_t channels, int32_t val) +{ + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "lm_static_apply_switch: NULL processing module"); + return -EINVAL; + } + + struct level_multiplier_comp_data *cd = module_get_private_data(mod); + + if (!cd) { + comp_err(dev, "lm_static_apply_switch: NULL private data"); + return -EINVAL; + } + + cd->gain = (val == 0) ? 0 : LEVEL_MULTIPLIER_GAIN_ONE; + return 0; +} + +static struct sof_static_module_ops lm_static_ops = { + .uuid = &level_multiplier_uuid, + .apply_volume = lm_static_apply_volume, + .apply_switch = lm_static_apply_switch, +}; + +DECLARE_STATIC_MODULE_OPS(level_multiplier, &lm_static_ops); +#endif /* CONFIG_STATIC_PIPELINE */ + #endif diff --git a/src/audio/selector/selector.c b/src/audio/selector/selector.c index bc48f99b5126..10a6875e9733 100644 --- a/src/audio/selector/selector.c +++ b/src/audio/selector/selector.c @@ -1182,3 +1182,153 @@ SOF_MODULE_INIT(selector, sys_comp_module_selector_interface_init); #endif #endif + +#if CONFIG_STATIC_PIPELINE +#include +#include + +static struct comp_dev *sel_static_create(const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us) +{ +#if CONFIG_IPC_MAJOR_4 + /* + * In IPC4 mode, selector module_adapter init expects + * struct sof_selector_avs_ipc4_config containing base_cfg and output_format. + * Synthesizing this structure satisfies selector_init size validation. + */ + struct sof_selector_avs_ipc4_config sel_cfg; + + memset(&sel_cfg, 0, sizeof(sel_cfg)); + sof_static_init_base_cfg(&sel_cfg.base_cfg, cdesc, period_us); + sel_cfg.output_format = sel_cfg.base_cfg.audio_fmt; + + if (cdesc->caps.sink_rate) + sel_cfg.output_format.sampling_frequency = cdesc->caps.sink_rate; + + struct ipc_config_process sel_spec = { + .size = sizeof(sel_cfg), + .data = (const uint8_t *)&sel_cfg, + }; + return drv->ops.create(drv, cfg, &sel_spec); +#else + uint16_t ch = cdesc->caps.max_channels ? cdesc->caps.max_channels : 2; + struct sof_sel_config sel_cfg = { + .in_channels_count = ch, + .out_channels_count = ch, + .sel_channel = 0, + }; + struct ipc_config_process sel_spec = { + .size = sizeof(sel_cfg), + .data = (const uint8_t *)&sel_cfg, + }; + return drv->ops.create(drv, cfg, &sel_spec); +#endif +} + +static int sel_static_apply_enum(struct comp_dev *dev, uint32_t channel, int32_t val) +{ + /* Validate component pointer */ + if (!dev) + return -EINVAL; + + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "sel_static_apply_enum: NULL processing module"); + return -EINVAL; + } + + struct comp_data *cd = module_get_private_data(mod); + + if (!cd) { + comp_err(dev, "sel_static_apply_enum: NULL private data"); + return -EINVAL; + } + + /* Validate selection value: must be non-negative */ + if (val < 0) { + comp_err(dev, "sel_static_apply_enum: negative sel_channel %d", val); + return -EINVAL; + } + + /* Query source producer channel count or fallback to configured input width */ + uint32_t src_channels = 0; + struct comp_buffer *src = comp_dev_get_first_data_producer(dev); + if (src) + src_channels = audio_stream_get_channels(&src->stream); + else + src_channels = cd->config.in_channels_count; + + /* Validate selection against hardware ceiling and input stream channel width */ + if ((uint32_t)val >= SEL_SOURCE_CHANNELS_MAX || (src_channels && (uint32_t)val >= src_channels)) { + comp_err(dev, "sel_static_apply_enum: invalid sel_channel %d (source channels %u)", + val, src_channels); + return -EINVAL; + } + + /* Apply validated channel selection to runtime configuration */ + cd->config.sel_channel = (uint32_t)val; + +#if CONFIG_IPC_MAJOR_4 + /* + * In IPC4 mode, routing is controlled by the Q10 coefficient matrix cd->coeffs_config. + * If channel indicates a single output channel (channel < out_channels_count), route + * input channel 'val' to that specific output channel. If channel indicates all + * channels (channel >= out_channels_count), route input channel 'val' to all output + * channels. + */ + uint32_t out_channels = cd->config.out_channels_count ? + cd->config.out_channels_count : SEL_SINK_CHANNELS_MAX; + + if (channel < out_channels) { + /* Route selected input channel to the specified output channel */ + for (uint32_t in_ch = 0; in_ch < SEL_SOURCE_CHANNELS_MAX; in_ch++) + cd->coeffs_config.coeffs[channel][in_ch] = (in_ch == (uint32_t)val) ? + SEL_COEF_ONE_Q10 : 0; + } else { + /* Route selected input channel to all output channels */ + for (uint32_t out_ch = 0; out_ch < out_channels && out_ch < SEL_SINK_CHANNELS_MAX; out_ch++) { + for (uint32_t in_ch = 0; in_ch < SEL_SOURCE_CHANNELS_MAX; in_ch++) + cd->coeffs_config.coeffs[out_ch][in_ch] = (in_ch == (uint32_t)val) ? + SEL_COEF_ONE_Q10 : 0; + } + } + + /* + * Re-evaluate passthrough flag: passthrough is true only when input and + * output channel counts match and coeffs form an identity matrix. + */ + if (cd->config.in_channels_count == cd->config.out_channels_count && + cd->config.in_channels_count > 0) { + cd->passthrough = true; + for (uint32_t i = 0; i < cd->config.out_channels_count; i++) { + for (uint32_t j = 0; j < cd->config.in_channels_count; j++) { + int16_t c = cd->coeffs_config.coeffs[i][j]; + + if ((i == j && c != SEL_COEF_ONE_Q10) || (i != j && c != 0)) { + cd->passthrough = false; + break; + } + } + if (!cd->passthrough) + break; + } + } else { + cd->passthrough = false; + } +#endif + + return 0; +} + +#if CONFIG_IPC_MAJOR_4 +static struct sof_static_module_ops sel4_static_ops = { + .uuid = &selector4_uuid, + .create = sel_static_create, + .apply_enum = sel_static_apply_enum, +}; +DECLARE_STATIC_MODULE_OPS(selector4, &sel4_static_ops); +#endif +#endif /* CONFIG_STATIC_PIPELINE */ diff --git a/src/audio/src/src.c b/src/audio/src/src.c index 9870ad911ca8..32470404f52d 100644 --- a/src/audio/src/src.c +++ b/src/audio/src/src.c @@ -107,4 +107,35 @@ SOF_LLEXT_BUILDINFO; DECLARE_MODULE_ADAPTER(src_interface, SRC_UUID, src_tr); SOF_MODULE_INIT(src, sys_comp_module_src_interface_init); +#if CONFIG_STATIC_PIPELINE +#include + +#if CONFIG_IPC_MAJOR_4 +static struct comp_dev *src_static_create(const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us) +{ + struct ipc4_config_src src_cfg; + + memset(&src_cfg, 0, sizeof(src_cfg)); + sof_static_init_base_cfg(&src_cfg.base, cdesc, period_us); + src_cfg.sink_rate = cdesc->caps.sink_rate ? cdesc->caps.sink_rate : cdesc->caps.default_rate; + + struct ipc_config_process spec = { + .size = sizeof(src_cfg), + .data = (const uint8_t *)&src_cfg, + }; + return drv->ops.create(drv, cfg, &spec); +} + +static struct sof_static_module_ops src_static_ops = { + .uuid = &SRC_UUID, + .create = src_static_create, +}; + +DECLARE_STATIC_MODULE_OPS(src, &src_static_ops); +#endif /* CONFIG_IPC_MAJOR_4 */ +#endif /* CONFIG_STATIC_PIPELINE */ + #endif diff --git a/src/audio/tdfb/tdfb.c b/src/audio/tdfb/tdfb.c index eed22f395632..6dc94409977f 100644 --- a/src/audio/tdfb/tdfb.c +++ b/src/audio/tdfb/tdfb.c @@ -945,4 +945,40 @@ DECLARE_TR_CTX(tdfb_tr, SOF_UUID(tdfb_uuid), LOG_LEVEL_INFO); DECLARE_MODULE_ADAPTER(tdfb_interface, tdfb_uuid, tdfb_tr); SOF_MODULE_INIT(tdfb, sys_comp_module_tdfb_interface_init); +#if CONFIG_STATIC_PIPELINE +#include + +static int tdfb_static_apply_switch(struct comp_dev *dev, uint32_t channels, int32_t val) +{ + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "tdfb_static_apply_switch: NULL processing module"); + return -EINVAL; + } + + struct tdfb_comp_data *cd = module_get_private_data(mod); + + if (!cd) { + comp_err(dev, "tdfb_static_apply_switch: NULL private data"); + return -EINVAL; + } + + /* + * Update beamforming enable state and flag that FIR setup needs + * to be recalculated in the next tdfb_process() cycle. + */ + cd->beam_on = (val != 0); + cd->update = true; + return 0; +} + +static struct sof_static_module_ops tdfb_static_ops = { + .uuid = &tdfb_uuid, + .apply_switch = tdfb_static_apply_switch, +}; + +DECLARE_STATIC_MODULE_OPS(tdfb, &tdfb_static_ops); +#endif /* CONFIG_STATIC_PIPELINE */ + #endif diff --git a/src/audio/volume/volume.c b/src/audio/volume/volume.c index 65d4d1e674e6..8dd2e55cc317 100644 --- a/src/audio/volume/volume.c +++ b/src/audio/volume/volume.c @@ -889,4 +889,170 @@ DECLARE_MODULE_ADAPTER(gain_interface, gain_uuid, gain_tr); SOF_MODULE_INIT(gain, sys_comp_module_gain_interface_init); #endif +#if CONFIG_STATIC_PIPELINE +#include + +#if CONFIG_IPC_MAJOR_4 +#include "peak_volume.h" + +struct static_ipc4_vol_init_cfg { + struct ipc4_base_module_cfg base_cfg; + struct ipc4_peak_volume_config config[1]; +}; +#endif + +static struct comp_dev *vol_static_create(const struct comp_driver *drv, + struct comp_ipc_config *cfg, + const struct sof_static_comp *cdesc, + uint32_t period_us) +{ + struct static_ipc4_vol_init_cfg vol_cfg; + + memset(&vol_cfg, 0, sizeof(vol_cfg)); + sof_static_init_base_cfg(&vol_cfg.base_cfg, cdesc, period_us); + vol_cfg.config[0].channel_id = 0xffffffff; + vol_cfg.config[0].target_volume = 0x7FFFFFFF; + vol_cfg.config[0].curve_type = IPC4_AUDIO_CURVE_TYPE_WINDOWS_FADE; + vol_cfg.config[0].curve_duration = 100000; + + struct ipc_config_process spec = { + .size = sizeof(vol_cfg), + .data = (const uint8_t *)&vol_cfg, + }; + return drv->ops.create(drv, cfg, &spec); +} + +static int vol_static_apply_volume(struct comp_dev *dev, uint32_t channels, int32_t val) +{ + /* Validate component pointer */ + if (!dev) + return -EINVAL; + + /* Validate channels count against allowed maximum */ + if (channels == 0 || channels > SOF_IPC_MAX_CHANNELS) { + comp_err(dev, "vol_static_apply_volume: invalid channel count %u", channels); + return -EINVAL; + } + + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "vol_static_apply_volume: NULL processing module"); + return -EINVAL; + } + + /* Apply volume gain value to each specified channel */ + for (uint32_t ch = 0; ch < channels; ch++) + volume_set_chan(mod, ch, val, true); + + return 0; +} + +static int vol_static_apply_volume_chan(struct comp_dev *dev, uint32_t channel, int32_t val) +{ + /* Validate component pointer */ + if (!dev) + return -EINVAL; + + /* Validate channel index against allowed maximum */ + if (channel >= SOF_IPC_MAX_CHANNELS) { + comp_err(dev, "vol_static_apply_volume_chan: invalid channel %u", channel); + return -EINVAL; + } + + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "vol_static_apply_volume_chan: NULL processing module"); + return -EINVAL; + } + + /* Apply volume gain value to single specified channel */ + volume_set_chan(mod, channel, val, true); + return 0; +} + +static int vol_static_apply_switch(struct comp_dev *dev, uint32_t channels, int32_t val) +{ + /* Validate component pointer */ + if (!dev) + return -EINVAL; + + /* Validate channels count against allowed maximum */ + if (channels == 0 || channels > SOF_IPC_MAX_CHANNELS) { + comp_err(dev, "vol_static_apply_switch: invalid channel count %u", channels); + return -EINVAL; + } + + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "vol_static_apply_switch: NULL processing module"); + return -EINVAL; + } + + /* Apply mute/unmute state to each specified channel */ + for (uint32_t ch = 0; ch < channels; ch++) { + if (val == 0) + volume_set_chan_mute(mod, ch); + else + volume_set_chan_unmute(mod, ch); + } + + return 0; +} + +static int vol_static_apply_switch_chan(struct comp_dev *dev, uint32_t channel, int32_t val) +{ + /* Validate component pointer */ + if (!dev) + return -EINVAL; + + /* Validate channel index against allowed maximum */ + if (channel >= SOF_IPC_MAX_CHANNELS) { + comp_err(dev, "vol_static_apply_switch_chan: invalid channel %u", channel); + return -EINVAL; + } + + struct processing_module *mod = comp_mod(dev); + + if (!mod) { + comp_err(dev, "vol_static_apply_switch_chan: NULL processing module"); + return -EINVAL; + } + + /* Apply mute/unmute state to single specified channel */ + if (val == 0) + volume_set_chan_mute(mod, channel); + else + volume_set_chan_unmute(mod, channel); + + return 0; +} + +static struct sof_static_module_ops vol_static_ops = { + .uuid = &volume_uuid, + .create = vol_static_create, + .apply_volume = vol_static_apply_volume, + .apply_volume_chan = vol_static_apply_volume_chan, + .apply_switch = vol_static_apply_switch, + .apply_switch_chan = vol_static_apply_switch_chan, +}; + +DECLARE_STATIC_MODULE_OPS(volume, &vol_static_ops); + +#if CONFIG_COMP_GAIN +static struct sof_static_module_ops gain_static_ops = { + .uuid = &gain_uuid, + .create = vol_static_create, + .apply_volume = vol_static_apply_volume, + .apply_volume_chan = vol_static_apply_volume_chan, + .apply_switch = vol_static_apply_switch, + .apply_switch_chan = vol_static_apply_switch_chan, +}; + +DECLARE_STATIC_MODULE_OPS(gain, &gain_static_ops); +#endif +#endif /* CONFIG_STATIC_PIPELINE */ + #endif From 2d670f719bf4c782f243e0cee03f6c2e470627c2 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 11 Oct 2026 13:45:47 +0100 Subject: [PATCH 7/7] doc: pipeline: add static pipeline architecture guide and documentation Add comprehensive documentation for the static audio pipeline subsystem in src/audio/pipeline/README.md covering: - Architectural motivations for microcontroller and hostless systems - Core concepts and declarative topology structures - Step-by-step tutorial with multi-rate SRC playback examples - Ops-driven module interface and UAC2 integration - Custom kcontrol handlers with named identifiers Signed-off-by: Liam Girdwood --- src/audio/pipeline/README.md | 248 ++++++++++++++++++++++++++++++++++- 1 file changed, 247 insertions(+), 1 deletion(-) diff --git a/src/audio/pipeline/README.md b/src/audio/pipeline/README.md index 4e530bdc1463..ad8aa6d4a27c 100644 --- a/src/audio/pipeline/README.md +++ b/src/audio/pipeline/README.md @@ -240,6 +240,252 @@ sequenceDiagram deactivate Sched ``` +## Static Pipeline Subsystem (Hostless / Standalone Execution) + +In microcontroller, hostless, or standalone embedded environments (e.g. ESP32-P4/S3/C6, Teensy 4.1 / i.MX RT1062, Nordic nRF54, RP2350, audio bridge appliances, smart speakers, standalone DSP dongles), there is no dynamic IPC host (Linux ALSA/SoundWire or Windows driver) to construct audio topologies at runtime. + +The Static Pipeline subsystem (`CONFIG_STATIC_PIPELINE`) provides a declarative C API (``) and loader engine (`static_pipeline_loader.c`) that instantiates full SOF processing graphs directly at boot: + +```mermaid +graph TD + Topo["Declarative Topology Descriptor
(struct sof_static_topology)"] --> Loader["Static Topology Loader
(sof_static_topology_init)"] + Loader --> PPL["Native Pipelines
(pipeline_new)"] + Loader --> COMPS["Components
(DAI, USB/Host, Modules)"] + Loader --> BUFS["Intermediate Buffers
(buffer_alloc)"] + Loader --> ROUTES["Graph Connections
(pipeline_connect)"] + Loader --> CONTROLS["Kcontrols & Custom Callbacks
(Volume, Mute, EQ Bypass, Routing)"] +``` + +### Key Capabilities & Architecture + +1. **Declarative Definitions**: Topologies describe pipelines, components (modules, DAI, USB/Host endpoints), intermediate buffers, routes, PCMs, and kcontrols using builder macros (`SOF_STATIC_MODULE`, `SOF_STATIC_ENDPOINT_DAI`, `SOF_STATIC_ENDPOINT_USB`, `SOF_STATIC_BUFFER`, `SOF_STATIC_ROUTE`, `SOF_STATIC_KCONTROL_*`). +2. **Ops-Driven Module Architecture**: Generic headers and the loader engine are completely decoupled from individual audio modules. Each audio processing component defines and registers operations (`struct sof_static_module_ops`) providing `.create`, `.apply_volume`, `.apply_switch`, and `.apply_enum` callbacks directly in its own module source file via `DECLARE_STATIC_MODULE_OPS()`. +3. **Explicit Parameter Initialization**: Synchronously negotiates stream parameters and prepares all graph components (`comp_params`, `comp_prepare`) to bring non-host-driven pipelines into ready state before audio streaming begins. +4. **Trigger & State Synchronization**: Provides direct runtime pipeline control (`sof_static_pipeline_trigger()`, `sof_static_pipeline_trigger_by_uac2_term()`) with state validation, synchronous trigger execution, per-component trigger propagation, and scheduler copy task activation/cancellation. +5. **Decoupled Control Callbacks**: Standard kcontrols dispatch directly to module ops (Volume, Mute, Level Multiplier, EQ bypass, DRC compression switch, Selector channel), while custom platform or board-level controls (clock mode switching, hardware routing, DMIC injectors) are handled cleanly via `custom_control_handler` callbacks. + +### Usage Guide & Tutorial + +#### 1. Defining Components + +Declare processing modules, host/USB endpoints, and hardware DAIs using either concise constructor macros or explicit struct declarations: + +```c +#include + +extern const struct sof_uuid src_uuid; +extern const struct sof_uuid volume_uuid; +extern const struct sof_uuid eq_iir_uuid; + +static const struct sof_static_comp s_comps[] = { + /* Playback Endpoint (USB UAC2 Terminal ID 1) @ 44.1 kHz */ + SOF_STATIC_ENDPOINT_USB( + 1, 1, "USB_PB", SOF_IPC_STREAM_PLAYBACK, + SOF_IPC_FRAME_S16_LE, 44100, 2, 1 + ), + /* Sample Rate Converter: 44.1 kHz In -> 48 kHz Out */ + SOF_STATIC_MODULE_RATE_CONV( + 2, 1, "SRC_PB", SOF_IPC_STREAM_PLAYBACK, &src_uuid, + SOF_IPC_FRAME_S16_LE, 44100, 48000, 2 + ), + /* Volume Control Module @ 48 kHz */ + SOF_STATIC_MODULE( + 3, 1, "VOL_PB", SOF_IPC_STREAM_PLAYBACK, &volume_uuid, + SOF_IPC_FRAME_S16_LE, 48000, 2, NULL, 0 + ), + /* 4-Band Parametric IIR EQ Module @ 48 kHz */ + SOF_STATIC_MODULE( + 4, 1, "EQ_PB", SOF_IPC_STREAM_PLAYBACK, &eq_iir_uuid, + SOF_IPC_FRAME_S16_LE, 48000, 2, NULL, 0 + ), + /* Hardware I2S DAI Output @ 48 kHz */ + SOF_STATIC_ENDPOINT_DAI( + 5, 1, "I2S_TX", SOF_IPC_STREAM_PLAYBACK, + SOF_IPC_FRAME_S16_LE, 48000, 2, + SOF_DAI_INTEL_SSP, 0, SOF_DAI_FMT_I2S | SOF_DAI_FMT_CBC_CFC + ), +}; +``` + +#### 2. Defining Intermediate Buffers + +Allocate audio buffers connecting consecutive processing blocks. In multi-rate pipelines (such as with SRC / ASRC), buffers can declare their individual sample rates and channel counts or inherit them dynamically from the upstream producer: + +```c +static const struct sof_static_buffer s_buffers[] = { + /* Buffer between USB_PB (1) and SRC_PB (2) @ 44.1 kHz */ + SOF_STATIC_BUFFER( + .id = 1, + .size = 45 * 2 * sizeof(int16_t) * 4, /* 4 periods @ 44.1 kHz */ + .fmt = SOF_IPC_FRAME_S16_LE, + .rate = 44100, + .channels = 2, + ), + /* Buffer between SRC_PB (2) and VOL_PB (3) @ 48 kHz */ + SOF_STATIC_BUFFER( + .id = 2, + .size = 48 * 2 * sizeof(int16_t) * 4, /* 4 periods @ 48 kHz */ + .fmt = SOF_IPC_FRAME_S16_LE, + .rate = 48000, + .channels = 2, + ), + /* Buffer between VOL_PB (3) and EQ_PB (4) @ 48 kHz */ + SOF_STATIC_BUFFER( + .id = 3, + .size = 48 * 2 * sizeof(int16_t) * 4, + .fmt = SOF_IPC_FRAME_S16_LE, + .rate = 48000, + .channels = 2, + ), + /* Buffer between EQ_PB (4) and I2S_TX (5) @ 48 kHz */ + SOF_STATIC_BUFFER( + .id = 4, + .size = 48 * 2 * sizeof(int16_t) * 4, + .fmt = SOF_IPC_FRAME_S16_LE, + .rate = 48000, + .channels = 2, + ), +}; +``` + +#### 3. Defining Graph Routes + +Connect component outputs through intermediate buffers to component inputs: + +```c +static const struct sof_static_route s_routes[] = { + SOF_STATIC_ROUTE(.src_comp_id = 1, .buffer_id = 1, .sink_comp_id = 2), + SOF_STATIC_ROUTE(.src_comp_id = 2, .buffer_id = 2, .sink_comp_id = 3), + SOF_STATIC_ROUTE(.src_comp_id = 3, .buffer_id = 3, .sink_comp_id = 4), + SOF_STATIC_ROUTE(.src_comp_id = 4, .buffer_id = 4, .sink_comp_id = 5), +}; +``` + +#### 4. Defining Top-Level Pipelines + +Specify pipeline execution properties, scheduling domains, and boundary endpoints: + +```c +static const struct sof_static_pipeline_desc s_pipelines[] = { + { + .pipeline_id = 1, + .name = "Playback Pipeline", + .direction = SOF_IPC_STREAM_PLAYBACK, + .priority = 0, + .core = 0, + .period = 1000, /* 1 ms tick */ + .frames_per_sched = 48, /* 48 frames per ms @ 48 kHz */ + .time_domain = SOF_TIME_DOMAIN_TIMER, + .default_rate = 48000, /* Pipeline target rate */ + .default_channels = 2, + .sched_comp_id = 5, /* Driven by I2S DAI */ + .source_comp_id = 1, /* Ingress: USB_PB */ + .sink_comp_id = 5, /* Egress: I2S_TX */ + }, +}; +``` + +#### 5. Defining Kcontrols & Custom Handlers + +Declare runtime controls (volume, mute, filter bypass switches) and optional platform callbacks: + +```c +enum { + CTRL_ID_VOLUME = 1, + CTRL_ID_EQ_BYPASS = 2, + CTRL_ID_CLOCK_MODE = 3, +}; + +static const struct sof_static_kcontrol s_controls[] = { + SOF_STATIC_KCONTROL_VOLUME( + .id = CTRL_ID_VOLUME, + .name = "Master Playback Volume", + .target_comp_id = 3, /* Targets VOL_PB (3) */ + .min = 0, + .max = INT32_MAX, + .def = INT32_MAX, /* 0 dB unity gain default (Q1.31) */ + .channels = 2, + .uac2_entity_id = 10, /* Bound to UAC2 Feature Unit 10 */ + ), + SOF_STATIC_KCONTROL_SWITCH( + .id = CTRL_ID_EQ_BYPASS, + .name = "EQ Bypass Switch", + .target_comp_id = 4, /* Targets EQ_PB (4, 0=bypass, 1=active) */ + .min = 0, + .max = 1, + .def = 1, + .channels = 1, + ), + SOF_STATIC_KCONTROL_SWITCH( + .id = CTRL_ID_CLOCK_MODE, + .name = "Hardware Clock Mode", + .target_comp_id = 0, /* Custom control (dispatched to callback) */ + .min = 0, + .max = 1, + .def = 1, /* 1 = Master, 0 = Slave */ + .channels = 1, + ), +}; + +static int platform_control_callback(const struct sof_static_kcontrol *ctl, + int32_t val, void *priv) +{ + if (ctl->id == CTRL_ID_CLOCK_MODE) { + /* Reconfigure physical I2S clock mode between Master and Slave */ + return platform_set_i2s_clock_mode(val == 1); + } + return -EINVAL; +} + +const struct sof_static_topology g_my_platform_topology = { + .name = "Demo Playback Topology", + .num_pipelines = ARRAY_SIZE(s_pipelines), + .pipelines = s_pipelines, + .num_comps = ARRAY_SIZE(s_comps), + .comps = s_comps, + .num_buffers = ARRAY_SIZE(s_buffers), + .buffers = s_buffers, + .num_routes = ARRAY_SIZE(s_routes), + .routes = s_routes, + .num_controls = ARRAY_SIZE(s_controls), + .controls = s_controls, + .custom_control_handler = platform_control_callback, + .custom_control_data = NULL, +}; +``` + +#### 6. Instantiating and Running the Topology + +At board startup, call `sof_static_topology_init()`: + +```c +#include + +int my_platform_init(void) +{ + /* Build and prepare audio pipeline graph */ + int ret = sof_static_topology_init(&g_my_platform_topology); + if (ret < 0) { + LOG_ERR("Failed to initialize static topology: %d", ret); + return ret; + } + + /* Start audio streaming */ + sof_static_pipeline_trigger(1, true); + return 0; +} +``` + +#### 7. Runtime Control APIs + +- **`sof_static_kcontrol_set(uint32_t ctrl_id, int32_t val)`**: Updates volume, mute, or bypass state at runtime. +- **`sof_static_kcontrol_get(uint32_t ctrl_id, int32_t *val)`**: Reads current control value. +- **`sof_static_pipeline_start(uint32_t pipeline_id)`**: Prepares and starts a pipeline synchronously. +- **`sof_static_pipeline_stop(uint32_t pipeline_id)`**: Stops and pauses an active pipeline synchronously. +- **`sof_static_pipeline_trigger(uint32_t pipeline_id, bool start)`**: Starts or stops a pipeline synchronously. +- **`sof_static_kcontrol_set_by_uac2(uint8_t entity_id, uint8_t ch, int32_t val, bool is_volume)`**: Automatically translates USB UAC2 8.8 dB fader commands to SOF native volume values. + ## Configuration and Scripts -* **CMakeLists.txt**: Straightforward build configuration integrating the fundamental internal execution blocks of the SOF graph: `pipeline-graph.c`, `pipeline-stream.c`, `pipeline-params.c`, `pipeline-xrun.c`, and `pipeline-schedule.c`. +* **CMakeLists.txt**: Build configuration integrating internal execution blocks of the SOF graph (`pipeline-graph.c`, `pipeline-stream.c`, `pipeline-params.c`, `pipeline-xrun.c`, `pipeline-schedule.c`) and conditionally compiling `static_pipeline_loader.c`, `static_pipeline_modules.c`, and `static_pipeline_uac2.c` when `CONFIG_STATIC_PIPELINE=y`. +* **Endpoint Callbacks**: Hardware DAI endpoints can attach optional platform-specific clock and hardware configuration callbacks via `SOF_STATIC_ENDPOINT_DAI_CFG()` without polluting the generic loader with SoC-specific code.