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 2965a7902301fc11309f62a609e607f9d5b45253 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 | 858 ++++++++++++++++++ 1 file changed, 858 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..514450db6bfc --- /dev/null +++ b/src/include/sof/audio/pipeline/static_pipeline.h @@ -0,0 +1,858 @@ +/* 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, + struct sof_ipc_dai_config *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); + + /** 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) \ + __attribute__((constructor)) __used \ + static 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 */ + 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. + */ +#define SOF_STATIC_BUFFER(...) \ + { .flags = SOF_MEM_FLAG_DMA | SOF_MEM_FLAG_USER, __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 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 Set the stream sample rate for a specific static pipeline. + * + * Updates the configured sample rate for the designated pipeline and its + * constituent components and buffers. + * + * \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); + +/** + * \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 f68eab5b785609275bd7a098026f6b75b22307b7 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 | 267 +++++++++++++++++++ src/audio/pipeline/static_pipeline_modules.h | 45 ++++ 2 files changed, 312 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..cfdb928b3f84 --- /dev/null +++ b/src/audio/pipeline/static_pipeline_modules.c @@ -0,0 +1,267 @@ +// 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 "static_pipeline_modules.h" + +LOG_MODULE_REGISTER(static_pipeline_modules, CONFIG_SOF_LOG_LEVEL); + +/* ========================================================================= + * Static Module Operations Registry + * ========================================================================= */ + +static 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; + 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: + /* + * Unsupported channel counts (> 8 or 0) are rejected and clamped + * to a valid stereo configuration to maintain internally consistent metadata. + */ + LOG_ERR("sof_static_init_base_cfg: unsupported channel count %u, clamping to stereo", + channels); + 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 268d1460fbe0bc2e66a146bcdc3c89c3494ac934 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 | 43 + src/audio/pipeline/CMakeLists.txt | 5 + src/audio/pipeline/static_pipeline_loader.c | 1492 +++++++++++++++++++ 3 files changed, 1540 insertions(+) create mode 100644 src/audio/pipeline/static_pipeline_loader.c diff --git a/src/audio/Kconfig b/src/audio/Kconfig index 8accb25738a2..d836323d76fe 100644 --- a/src/audio/Kconfig +++ b/src/audio/Kconfig @@ -51,6 +51,49 @@ 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 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..95651ddbceae --- /dev/null +++ b/src/audio/pipeline/static_pipeline_loader.c @@ -0,0 +1,1492 @@ +// 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 "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 + +#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 + +/* Active declarative topology descriptor */ +static const struct sof_static_topology *s_active_topo; + +/* Static tracking tables for fast ID-to-pointer lookups */ +static struct pipeline *s_pipelines[MAX_STATIC_PIPELINES]; +static uint32_t s_pipeline_ids[MAX_STATIC_PIPELINES]; +static size_t s_num_pipelines; + +static struct comp_dev *s_comps[MAX_STATIC_COMPS]; +static uint32_t s_comp_ids[MAX_STATIC_COMPS]; +static size_t s_num_comps; + +static struct comp_buffer *s_buffers[MAX_STATIC_BUFFERS]; +static uint32_t s_buffer_ids[MAX_STATIC_BUFFERS]; +static size_t s_num_buffers; + +/* Cached kcontrol values */ +static int32_t s_control_vals[MAX_STATIC_CONTROLS]; + +/* Tracked stream sample rates in Hz per static pipeline */ +static uint32_t s_pipeline_rates[MAX_STATIC_PIPELINES]; + +/** + * \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 = get_sample_bitdepth(fmt) / 8; + + /* 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->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 ? pdesc->time_domain : SOF_TIME_DOMAIN_TIMER; + + /* 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) +{ + /* 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(s_active_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; + } + + struct sof_ipc_dai_config spec_cfg = { + .type = cdesc->ep.dai.dai_type, + .dai_index = cdesc->ep.dai.dai_index, + .format = cdesc->ep.dai.format, + }; + + /* Invoke optional platform/endpoint-specific configuration callback */ + if (cdesc->ep.dai.configure) { + int ret_cfg = cdesc->ep.dai.configure(dev, &dai_cfg, &spec_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) { + comp_dai_config(dd, dev, &dai_cfg, &spec_cfg); + if (!dd->dai_spec_config) { + dd->dai_spec_config = sof_heap_alloc(dd->alloc_ctx.heap, + SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + sizeof(struct sof_ipc_dai_config), 0); + if (dd->dai_spec_config) + memcpy(dd->dai_spec_config, &spec_cfg, sizeof(struct sof_ipc_dai_config)); + } + } + + 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, + const 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; + } + + /* Populate standard SOF component IPC configuration */ + struct comp_ipc_config cfg = { + .id = cdesc->id, + .pipeline_id = cdesc->pipeline_id, + .core = 0, + .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); + 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); + + /* Find owning pipeline descriptor to resolve scheduling period */ + const struct sof_static_pipeline_desc *pdesc = + sof_static_find_pipeline_desc(topo, cdesc->pipeline_id); + + 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) + 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) + 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 */ + pipeline_connect(src, buf, PPL_CONN_DIR_COMP_TO_BUFFER); + pipeline_connect(sink, buf, PPL_CONN_DIR_BUFFER_TO_COMP); + 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) + continue; + + /* 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); + + /* 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 = get_sample_bitdepth(fmt) / 8; + 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 = get_sample_bitdepth(fmt) / 8; + 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. + */ +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; + } + + LOG_INF("=== Loading Static Audio Topology: '%s' ===", topo->name ? topo->name : "Unnamed"); + s_active_topo = topo; + + /* 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); + return ret; + } + + /* 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); + return ret; + } + + /* 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); + return ret; + } + + /* 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); + return ret; + } + + /* 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); + return ret; + } + + /* 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); + return ret; + } + + /* 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); + return ret; + } + + 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; +} + +/** + * \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; + + /* 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; + + /* Dispatch control value to target component using registered operations */ + if (ops) { + int ret_op = 0; + + 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); + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set: apply_volume failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + } + 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); + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set: apply_switch failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + } + break; + + case SOF_STATIC_CTRL_ENUM: + /* Enumerated route or channel selector */ + if (ops->apply_enum) { + ret_op = ops->apply_enum(dev, 0, val); + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set: apply_enum failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + } + break; + + default: + LOG_ERR("sof_static_kcontrol_set: invalid control type %d for ctl %u", + ctl->type, ctl->id); + return -EINVAL; + } + } + + /* 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; + } + + /* 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; + + /* Dispatch control value to target component using channel-specific ops if available */ + if (ops) { + int ret_op = 0; + + 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); + } + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set_chan: apply_volume_chan failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + 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); + } + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set_chan: apply_switch_chan failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + break; + + case SOF_STATIC_CTRL_ENUM: + /* Channel / stream multiplexer selector */ + if (ops->apply_enum) { + ret_op = ops->apply_enum(dev, channel, val); + if (ret_op < 0) { + LOG_ERR("sof_static_kcontrol_set_chan: apply_enum failed %d for ctl [%u] '%s'", + ret_op, ctl->id, ctl->name); + return ret_op; + } + } + break; + + default: + LOG_ERR("sof_static_kcontrol_set_chan: invalid control type %d for ctl %u", + ctl->type, ctl->id); + return -EINVAL; + } + } + + /* 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 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 */ + 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; + } + + /* Propagate START trigger across the pipeline graph */ + ret = pipeline_trigger_run(pipe, dev, COMP_TRIGGER_START); + if (ret < 0) { + LOG_ERR("Pipeline %u start trigger failed: %d", pipeline_id, ret); + return ret; + } + + /* Explicitly notify all constituent components and mark state ACTIVE */ + if (s_active_topo) { + for (size_t j = 0; j < s_active_topo->num_comps; j++) { + if (s_active_topo->comps[j].pipeline_id == pipeline_id) { + struct comp_dev *c = sof_static_comp_get(s_active_topo->comps[j].id); + if (c) { + comp_trigger(c, COMP_TRIGGER_PRE_START); + comp_trigger(c, COMP_TRIGGER_START); + c->state = COMP_STATE_ACTIVE; + } + } + } + } + + 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; + } + + /* Explicitly notify all constituent components and mark state PAUSED */ + if (s_active_topo) { + for (size_t j = 0; j < s_active_topo->num_comps; j++) { + if (s_active_topo->comps[j].pipeline_id == pipeline_id) { + struct comp_dev *c = sof_static_comp_get(s_active_topo->comps[j].id); + if (c) { + comp_trigger(c, COMP_TRIGGER_STOP); + c->state = COMP_STATE_PAUSED; + } + } + } + } + + 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 Set the stream sample rate for a specific static pipeline. + * \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) +{ + for (size_t i = 0; i < s_num_pipelines; i++) { + if (s_pipeline_ids[i] == pipeline_id) { + s_pipeline_rates[i] = rate; + LOG_INF("Static pipeline %u sample rate set to %u Hz", pipeline_id, rate); + return 0; + } + } + + LOG_ERR("sof_static_pipeline_set_sample_rate: pipeline %u not found", pipeline_id); + return -ENOENT; +} + +/** + * \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; + } + + for (size_t i = 0; i < s_num_pipelines; i++) + s_pipeline_rates[i] = rate; + + LOG_INF("All static pipelines sample rate set to %u Hz", rate); + return 0; +} + +/** + * \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 b230284355b5b0fdf64138e7bfb60177f80e8113 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 | 286 ++++++++++++++++++++++ 2 files changed, 287 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..3178b2c42a42 --- /dev/null +++ b/src/audio/pipeline/static_pipeline_uac2.c @@ -0,0 +1,286 @@ +// 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 + +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 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); + + if (channel == 0) { + /* Master channel: update all channels in cache and hardware */ + if (cache) { + for (uint32_t ch = 0; ch <= ctl->channels && ch <= SOF_IPC_MAX_CHANNELS; ch++) + cache->val[ch] = val; + } + return sof_static_kcontrol_set(ctl->id, sof_vol); + } + + /* Individual channel: update specific channel in cache and hardware */ + if (cache && channel <= SOF_IPC_MAX_CHANNELS) + cache->val[channel] = val; + + return sof_static_kcontrol_set_chan(ctl->id, channel - 1, sof_vol); + } 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; + + if (channel == 0) { + /* Master channel: update all channels in cache and hardware */ + if (cache) { + for (uint32_t ch = 0; ch <= ctl->channels && ch <= SOF_IPC_MAX_CHANNELS; ch++) + cache->val[ch] = val; + } + return sof_static_kcontrol_set(ctl->id, sof_sw); + } + + /* Individual channel: update specific channel in cache and hardware */ + if (cache && channel <= SOF_IPC_MAX_CHANNELS) + cache->val[channel] = val; + + return sof_static_kcontrol_set_chan(ctl->id, channel - 1, sof_sw); + } + } + } + + 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 8a7a04fef6c485b2f0cefce6bb82d20347ebb7e4 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 | 35 ++++ src/audio/drc/drc.c | 33 ++++ src/audio/eq_iir/eq_iir.c | 87 +++++++++ src/audio/level_multiplier/level_multiplier.c | 52 ++++++ src/audio/selector/selector.c | 78 ++++++++ src/audio/src/src.c | 31 ++++ src/audio/tdfb/tdfb.c | 31 ++++ src/audio/volume/volume.c | 166 ++++++++++++++++++ 8 files changed, 513 insertions(+) diff --git a/src/audio/asrc/asrc.c b/src/audio/asrc/asrc.c index f75009ffd775..78e6041b0d94 100644 --- a/src/audio/asrc/asrc.c +++ b/src/audio/asrc/asrc.c @@ -846,4 +846,39 @@ 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); + + 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..04a234b5cc42 100644 --- a/src/audio/level_multiplier/level_multiplier.c +++ b/src/audio/level_multiplier/level_multiplier.c @@ -204,4 +204,56 @@ 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; + } + + cd->gain = val; + 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..dc1c39c26bd6 100644 --- a/src/audio/selector/selector.c +++ b/src/audio/selector/selector.c @@ -1182,3 +1182,81 @@ 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) +{ + 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); +} + +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_4CH || (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; + 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..b2f97bda01b2 100644 --- a/src/audio/tdfb/tdfb.c +++ b/src/audio/tdfb/tdfb.c @@ -945,4 +945,35 @@ 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; + } + + cd->beam_on = (val != 0); + 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 a9ed44c17dff0f89be0e5d626ba7b4a9ee1d818c 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.