Module Framework Architecture#

The Audio Processing Module Framework provides the standardized component interface and execution environment for all signal processing algorithms in Sound Open Firmware (SOF). By decoupling audio algorithms from low-level RTOS scheduling primitives, hardware platform drivers, and inter-processor communication (IPC) protocols, the module framework enables signal processing engineers to write portable, reusable audio processing blocks.

This architecture supports both statically linked in-tree processing modules (Volume, Equalizers, Mixers, Sample Rate Converters) and dynamically loaded third-party proprietary libraries (via Zephyr LLEXT), ensuring strict memory sandboxing and automated leak protection.

1. Architecture & Three-Tier Model#

The SOF module architecture is organized into three distinct tiers: the Standardized Module Interface, the Runtime Processing Module Instance, and the Module Adapter:

digraph module_architecture { rankdir=TB; nodesep=0.3; ranksep=0.4; node [shape=box, style="filled,rounded", fontname="Verdana", fontsize=9, margin="0.12,0.06"]; edge [fontname="Verdana", fontsize=8, color="#333333"]; subgraph cluster_sched { label = "Tier 1: SOF Core Pipeline Schedulers"; style = "filled,rounded"; color = "#2980b9"; fillcolor = "#ebf5fb"; fontname = "Verdana-Bold"; fontsize = 10; fontcolor = "#1b4f72"; ll_sched [label="Low-Latency (LL) Scheduler\n(1ms Hardware Timer / DMA Interrupts)", fillcolor="#aed6f1"]; dp_sched [label="Data Processing (DP) Scheduler\n(Asynchronous Zephyr RTOS Threads)", fillcolor="#aed6f1"]; } subgraph cluster_adapter { label = "Tier 2: Module Adapter System Layer (Sandbox & Proxy)"; style = "filled,rounded"; color = "#27ae60"; fillcolor = "#eafaf1"; fontname = "Verdana-Bold"; fontsize = 10; fontcolor = "#1e8449"; ma_proxy [label="Module Adapter Component Proxy\n(Masquerades as standard comp_dev)", fillcolor="#a9dfbf"]; ma_ipc [label="IPC Parameter & Config Dispatcher\n(Decodes Set/Get Value and Set/Get Data)", fillcolor="#a9dfbf"]; ma_mem [label="Memory Sandbox Manager\n(Component Heap & Object Pool Tracking)", fillcolor="#a9dfbf"]; } subgraph cluster_interface { label = "Tier 3: Standardized Module Framework & Processing APIs"; style = "filled,rounded"; color = "#8e44ad"; fillcolor = "#f4ecf7"; fontname = "Verdana-Bold"; fontsize = 10; fontcolor = "#512e5f"; mod_ops [label="Standardized Operations\n(init, prepare, process, reset, free)", fillcolor="#d7bde2"]; src_api [label="Source API (Inputs)\nsource_get_data / release", fillcolor="#d7bde2"]; snk_api [label="Sink API (Outputs)\nsink_get_buffer / commit", fillcolor="#d7bde2"]; } subgraph cluster_modules { label = "Concrete Audio Processing Modules"; style = "filled,rounded"; color = "#d35400"; fillcolor = "#fef5e7"; fontname = "Verdana-Bold"; fontsize = 10; fontcolor = "#a04000"; mod_vol [label="Volume / Mute\n(SIMD Vector Math)", fillcolor="#fad7a0"]; mod_eq [label="Parametric EQ\n(FIR / IIR Filters)", fillcolor="#fad7a0"]; mod_aec [label="Echo Cancellation\n(AEC / Beamformer)", fillcolor="#fad7a0"]; mod_dyn [label="Loadable Dynamic Module\n(Zephyr LLEXT / Vendor IP)", fillcolor="#f5b041", style="filled,bold"]; } ll_sched -> ma_proxy [label="Trigger / Copy"]; dp_sched -> ma_proxy [label="Thread Exec"]; ma_proxy -> mod_ops [label="Invokes"]; ma_proxy -> ma_mem [label="Manages"]; ma_ipc -> ma_proxy [label="IPC Events"]; mod_ops -> mod_vol; mod_ops -> mod_eq; mod_ops -> mod_aec; mod_ops -> mod_dyn; mod_vol -> src_api [style=dashed, label="Read"]; mod_vol -> snk_api [style=dashed, label="Write"]; mod_eq -> src_api [style=dashed, label="Read"]; mod_eq -> snk_api [style=dashed, label="Write"]; }

Figure 13 Three-Tier Architecture: Pipeline Schedulers to Concrete Audio Modules#

The Core Architectural Concepts#

  1. Standardized Module Operations (`module_interface`): A uniform set of function callbacks (init, prepare, process, reset, free, and set_configuration) that every audio algorithm must implement. Because the interface is generic, the algorithm requires no knowledge of whether it is running on a real-time interrupt tick, inside an asynchronous RTOS worker thread, or within an offline simulation testbench.

  2. Runtime Module Instance (`processing_module`): The runtime state of an instantiated module. It contains instance-specific metadata, negotiated audio format descriptors (sample rate, channel count, sample bit depth), memory pointers, and references to connected audio streams.

  3. Module Adapter (`module_adapter`): The architectural glue and sandboxing layer. To the pipeline scheduler, the adapter looks like a standard pipeline component. Internally, it manages the module’s lifecycle, allocates dedicated memory, handles parameter blobs from host IPC messages, and dispatches audio samples through standardized input and output APIs.

2. The Module Adapter & Sandboxing Container#

The Module Adapter wraps internal DSP kernels and third-party processing engines, acting as a secure protective sandbox between the untrusted algorithm and the core operating system:

digraph module_adapter_container { rankdir=LR; nodesep=0.3; ranksep=0.4; node [shape=box, style="filled,rounded", fontname="Verdana", fontsize=9]; edge [fontname="Verdana", fontsize=8, color="#333333"]; subgraph cluster_external { label = "Pipeline Environment"; style = "filled,rounded"; color = "#2c3e50"; fillcolor = "#ebedef"; fontname = "Verdana-Bold"; fontsize = 9; pipe_call [label="Pipeline Engine\n- Scheduling triggers\n- Buffer links", fillcolor="#d5dbdb"]; ipc_cmd [label="Host Driver IPC\n- Set/Get parameter blobs\n- Control sliders", fillcolor="#d5dbdb"]; } subgraph cluster_adapter_box { label = "Module Adapter Wrapper (Security & Isolation Boundary)"; style = "filled,rounded"; color = "#27ae60"; fillcolor = "#eafaf1"; fontname = "Verdana-Bold"; fontsize = 10; fontcolor = "#1e8449"; proxy_api [label="Component Interface Proxy\n- Intercepts comp_copy()\n- Intercepts comp_trigger()\n- Validates stream states", fillcolor="#a9dfbf"]; heap_mgr [label="Isolated Component Heap\n- Dedicated memory pool\n- Object pool tracking\n- Auto-free on teardown", fillcolor="#a9dfbf"]; ipc_trans [label="IPC Translation Engine\n- Deserializes config blobs\n- Bounds-checks buffer sizes\n- Dispatches to module ops", fillcolor="#a9dfbf"]; subgraph cluster_inner_mod { label = "Audio Processing Kernel"; style = "filled,rounded"; color = "#d35400"; fillcolor = "#fef5e7"; fontname = "Verdana-Bold"; fontsize = 9; fontcolor = "#a04000"; inner_state [label="Module Internal State\n- Filter delay lines\n- Biquad coefficients\n- Scratch memory buffers", fillcolor="#fad7a0"]; inner_kernel [label="Signal Processing Kernel\n(Pure Math / SIMD Transform)", fillcolor="#f39c12", fontcolor="#ffffff", style="filled,bold"]; inner_kernel -> inner_state; } proxy_api -> inner_kernel [label="Execute"]; ipc_trans -> inner_state [label="Apply Config"]; heap_mgr -> inner_state [label="Allocates"]; } pipe_call -> proxy_api [label="comp_copy()"]; ipc_cmd -> ipc_trans [label="IPC Config"]; }

Figure 14 Module Adapter Container: Encapsulation, State Control, and IPC Translation#

Adapter Responsibilities#

  • Scheduler Translation: Translates pipeline commands (comp_new, comp_prepare, comp_copy, comp_free) into clean module callbacks (init, prepare, process, free).

  • Memory Isolation: Restricts module allocations to dedicated component memory heaps so that third-party code cannot corrupt global RTOS heaps.

  • Leak Protection: Automatically logs and frees any lingering module memory allocations when the component is destroyed.

  • Format Negotiation: Checks that incoming audio formats meet the module’s declared mathematical constraints (e.g., verifying that a 16-bit module does not receive unformatted 32-bit floating-point data).

3. Standardized Processing Interface: Source & Sink APIs#

In traditional audio drivers, processing components often access circular ring buffer memory directly through raw pointers. This tightly couples the algorithm to buffer wrap-around mathematics and DMA alignment quirks.

The SOF Module Framework decouples algorithms from buffers through the Source and Sink APIs. Modules operate in a clean “Get → Manipulate → Commit/Release” execution flow:

digraph source_sink_flow { rankdir=TB; nodesep=0.25; ranksep=0.35; node [shape=box, style="filled,rounded", fontname="Verdana", fontsize=9]; edge [fontname="Verdana", fontsize=8, color="#333333"]; step1 [label="1. Module Triggered\nPipeline scheduler invokes module's process() entry point", fillcolor="#d4e6f1"]; step2 [label="2. Source Request (source_get_data)\nModule requests N input frames from the Source API\nSource API verifies available samples and returns a contiguous read pointer", fillcolor="#aed6f1"]; step3 [label="3. Sink Reservation (sink_get_buffer)\nModule requests N output frames from the Sink API\nSink API verifies available space and returns a contiguous write pointer", fillcolor="#aed6f1"]; step4 [label="4. Execute Audio Algorithm\nModule reads from read pointer, executes mathematical transformations,\nand writes processed samples to write pointer", fillcolor="#2ecc71", fontcolor="#ffffff", style="filled,bold"]; step5 [label="5. Source Release (source_release_data)\nModule notifies Source API of the exact number of frames consumed,\nadvancing the upstream read pointer", fillcolor="#abebc6"]; step6 [label="6. Sink Commit (sink_commit_buffer)\nModule notifies Sink API of the exact number of frames written,\nadvancing the downstream write pointer and validating data for consumers", fillcolor="#abebc6"]; step7 [label="7. Yield to Scheduler\nProcess operation returns status (success or error code) to the adapter", fillcolor="#d4e6f1"]; step1 -> step2 -> step3 -> step4 -> step5 -> step6 -> step7; }

Figure 15 Source and Sink API Execution Pattern#

Source API (Inputs)#

  • Modules request readable frames by invoking source_get_data().

  • The API abstracts circular buffer wrap-around, providing safe contiguous memory blocks.

  • Upon completing execution, the module calls source_release_data() with the exact number of frames consumed. If a module cannot process all available frames during this tick, unconsumed frames remain buffered for the next execution period.

Sink API (Outputs)#

  • Modules reserve writable space by invoking sink_get_buffer().

  • Once processed samples are written into the buffer, the module calls sink_commit_buffer() with the number of valid produced frames.

  • The commit operation makes the newly processed samples immediately visible to downstream components.

4. Pin Topologies & Stream Binding#

Audio modules connect to other components and buffers through directional pins:

  • Sink Pins (Inputs): Accept audio data streams from upstream components.

  • Source Pins (Outputs): Deliver processed audio streams to downstream components.

digraph pin_topologies { rankdir=LR; nodesep=0.3; ranksep=0.4; node [shape=box, style="filled,rounded", fontname="Verdana", fontsize=9]; edge [fontname="Verdana", fontsize=8, color="#333333"]; subgraph cluster_siso { label = "Single-Input Single-Output (SISO)"; style = "filled,rounded"; color = "#2980b9"; fillcolor = "#ebf5fb"; fontname = "Verdana-Bold"; fontsize = 9; siso_in [label="Input Buffer", shape=ellipse, fillcolor="#ffffff"]; siso_comp [label="In-Line Filter\n(Volume / EQ / DRC / SRC)", fillcolor="#aed6f1"]; siso_out [label="Output Buffer", shape=ellipse, fillcolor="#ffffff"]; siso_in -> siso_comp [label="Sink Pin 0"]; siso_comp -> siso_out [label="Source Pin 0"]; } subgraph cluster_miso { label = "Multi-Input Single-Output (MISO)"; style = "filled,rounded"; color = "#27ae60"; fillcolor = "#eafaf1"; fontname = "Verdana-Bold"; fontsize = 9; miso_in1 [label="Stream A Buffer", shape=ellipse, fillcolor="#ffffff"]; miso_in2 [label="Stream B Buffer", shape=ellipse, fillcolor="#ffffff"]; miso_comp [label="Audio Mixer\n(Summing Bus)", fillcolor="#a9dfbf"]; miso_out [label="Mixed Buffer", shape=ellipse, fillcolor="#ffffff"]; miso_in1 -> miso_comp [label="Sink Pin 0"]; miso_in2 -> miso_comp [label="Sink Pin 1"]; miso_comp -> miso_out [label="Source Pin 0"]; } subgraph cluster_simo { label = "Single-Input Multi-Output (SIMO)"; style = "filled,rounded"; color = "#8e44ad"; fillcolor = "#f4ecf7"; fontname = "Verdana-Bold"; fontsize = 9; simo_in [label="Multi-Ch Buffer", shape=ellipse, fillcolor="#ffffff"]; simo_comp [label="Demux / Splitter\n(Channel Router)", fillcolor="#d7bde2"]; simo_out1 [label="Ch 0-1 Buffer", shape=ellipse, fillcolor="#ffffff"]; simo_out2 [label="Ch 2-3 Buffer", shape=ellipse, fillcolor="#ffffff"]; simo_in -> simo_comp [label="Sink Pin 0"]; simo_comp -> simo_out1 [label="Source Pin 0"]; simo_comp -> simo_out2 [label="Source Pin 1"]; } }

Figure 16 Supported Module Pin Topologies#

Dynamic Pin Binding#

Pins are not hard-coded into the firmware executable; they are dynamically bound and unbound at runtime based on topology directives or host IPC commands:

  • Binding (`comp_bind`): Connects an upstream module’s source pin to a downstream module’s sink pin through an intermediate audio buffer.

  • Unbinding (`comp_unbind`): Safely detaches pins when an audio pipeline is torn down or rerouted.

5. Module Runtime State Machine#

Every processing module is strictly governed by a uniform runtime state machine managed by the module_adapter. Modules must adhere to the transitions defined by enum module_state:

digraph module_state_machine { rankdir=TB; nodesep=0.4; ranksep=0.4; node [shape=circle, style="filled", fontname="Verdana-Bold", fontsize=9, width=1.4, height=1.4, fixedsize=true]; edge [fontname="Verdana", fontsize=8, color="#2c3e50"]; node [fillcolor="#eaeded"] MODULE_DISABLED; node [fillcolor="#d4e6f1"] MODULE_INITIALIZED; node [fillcolor="#fcf3cf"] MODULE_IDLE; node [fillcolor="#abebc6"] MODULE_PROCESSING; MODULE_DISABLED -> MODULE_INITIALIZED [label="init()\n(Allocates scratch memory,\nparses init config)", color="#2980b9", fontcolor="#2980b9"]; MODULE_INITIALIZED -> MODULE_DISABLED [label="free()\n(Releases component heap)", color="#c0392b", fontcolor="#c0392b"]; MODULE_INITIALIZED -> MODULE_IDLE [label="prepare()\n(Negotiates sample rates,\nclears filter history)", color="#27ae60", fontcolor="#27ae60"]; MODULE_IDLE -> MODULE_INITIALIZED [label="reset()\n(Flushes stream history)", color="#f39c12", fontcolor="#b7950b"]; MODULE_IDLE -> MODULE_PROCESSING [label="trigger(START)\n(Begins audio processing)", color="#27ae60", fontcolor="#27ae60", penwidth=2]; MODULE_PROCESSING -> MODULE_IDLE [label="trigger(STOP / PAUSE)\n(Suspends processing)", color="#c0392b", fontcolor="#c0392b"]; }

Figure 17 Module Runtime State Transition Diagram#

State Definitions#

  • `MODULE_DISABLED`: The module is uninstantiated or has been freed. Zero memory or execution slots are allocated.

  • `MODULE_INITIALIZED`: The module has successfully executed its .init() callback. It has parsed static initialization configuration parameters and allocated necessary internal structures (delay lines, coefficient arrays).

  • `MODULE_IDLE`: The module has executed .prepare(). Stream formats (sample rates, channel maps, sample bit depths) are fully negotiated and agreed upon. The algorithm is ready to stream.

  • `MODULE_PROCESSING`: The pipeline has issued a START trigger. The module’s .process() function is actively transforming audio buffers on every scheduling tick.

6. Parameter & Configuration Management#

Audio processing components require dynamic runtime tuning—such as adjusting equalizer cutoffs, modifying compressor thresholds, or setting speaker protection parameters.

The Module Framework separates configuration into three primary delivery channels:

digraph config_dispatch { rankdir=LR; nodesep=0.3; ranksep=0.4; node [shape=box, style="filled,rounded", fontname="Verdana", fontsize=9]; edge [fontname="Verdana", fontsize=8, color="#333333"]; subgraph cluster_host { label = "Host User Space (ALSA / UCM2 / sof-ctl)"; style = "filled,rounded"; color = "#2980b9"; fillcolor = "#ebf5fb"; fontname = "Verdana-Bold"; fontsize = 9; host_init [label="Topology Manifest (.tplg)\n- Static filter defaults", fillcolor="#aed6f1"]; host_blob [label="Binary Coefficient Blob\n(e.g., 10-Band EQ Matrix)", fillcolor="#aed6f1"]; host_kctl [label="Mixer Control Switch\n(e.g., Volume Fader / Mute)", fillcolor="#aed6f1"]; } subgraph cluster_ipc { label = "IPC Messaging Gateway"; style = "filled,rounded"; color = "#d35400"; fillcolor = "#fef5e7"; fontname = "Verdana-Bold"; fontsize = 9; ipc_init [label="IPC Component New", fillcolor="#fad7a0"]; ipc_data [label="IPC Set Data (Large Payload)", fillcolor="#fad7a0"]; ipc_val [label="IPC Set Value (Immediate)", fillcolor="#fad7a0"]; } subgraph cluster_mod { label = "Processing Module"; style = "filled,rounded"; color = "#27ae60"; fillcolor = "#eafaf1"; fontname = "Verdana-Bold"; fontsize = 9; handler_init [label=".init() Parser\n(Applies default configuration)", fillcolor="#a9dfbf"]; handler_data [label=".set_configuration() Callback\n(Parses multi-byte filter blobs)", fillcolor="#a9dfbf"]; handler_kctl [label="Direct Control Binding\n(Updates gain / coefficients in-place)", fillcolor="#a9dfbf"]; } host_init -> ipc_init -> handler_init; host_blob -> ipc_data -> handler_data; host_kctl -> ipc_val -> handler_kctl; }

Figure 18 Configuration Dispatch: Static Blobs, Runtime Blobs, and Scalar Controls#

  1. Static Initialization Blobs: Delivered when the module is first instantiated via topology. Specifies initial configurations such as default filter modes or speaker models.

  2. Large Runtime Blobs (Set Data): Used for multi-kilobyte binary payloads (e.g., acoustic echo cancellation calibration matrices, custom FIR filter impulse responses). Delivered over shared host-DSP SRAM mailboxes.

  3. Immediate Scalar Values (Set Value): High-speed, lightweight commands used for volume faders, mute switches, or channel routing indices without allocation overhead.

7. Memory Sandboxing & Leak Protection#

To guarantee system stability, SOF isolates module allocations from global RTOS memory pools. This is especially vital when integrating third-party proprietary audio engines or dynamically loaded LLEXT modules:

digraph memory_sandboxing { rankdir=LR; nodesep=0.3; ranksep=0.4; node [shape=box, style="filled,rounded", fontname="Verdana", fontsize=9]; edge [fontname="Verdana", fontsize=8, color="#333333"]; subgraph cluster_global { label = "Global RTOS Memory Space"; style = "filled,rounded"; color = "#7f8c8d"; fillcolor = "#f2f4f4"; fontname = "Verdana-Bold"; fontsize = 9; sys_heap [label="System Global Heap\n(Kernel structs, DMA queues,\ninterrupt vectors)\n\nPROTECTED FROM MODULES", fillcolor="#d5dbdb", style="filled,bold"]; } subgraph cluster_sandbox { label = "Module Adapter Component Sandbox (dp_heap_user)"; style = "filled,rounded"; color = "#27ae60"; fillcolor = "#eafaf1"; fontname = "Verdana-Bold"; fontsize = 10; fontcolor = "#1e8449"; comp_heap [label="Dedicated Component Heap\n(Allocated per module instance)", fillcolor="#a9dfbf"]; subgraph cluster_objpool { label = "Object Pool (Tracking Table)"; style = "filled,rounded"; color = "#d35400"; fillcolor = "#fef5e7"; fontname = "Verdana-Bold"; fontsize = 8; obj1 [label="Tracked Block 1\n(Filter Delay Line)", fillcolor="#fad7a0"]; obj2 [label="Tracked Block 2\n(Coeff Matrix)", fillcolor="#fad7a0"]; obj3 [label="Tracked Block 3\n(Scratch Buffer)", fillcolor="#fad7a0"]; obj1 -> obj2 -> obj3 [style=invis]; } comp_heap -> obj1; comp_heap -> obj2; comp_heap -> obj3; } cleanup [label="Automated Cleanup (mod_free_all)\nOn component destruction, adapter iterates\nthrough Object Pool and frees all tracked\nallocations automatically", fillcolor="#abebc6", shape=note]; cluster_objpool -> cleanup [style=dashed, color="#27ae60"]; }

Figure 19 Memory Sandboxing: Global System Heap vs Component Heap with Object Tracking#

Memory Protection Features#

  • Isolated Allocation Pool (`dp_heap_user`): Modules allocate scratch buffers and persistent delay lines from their assigned component heap partition rather than competing with kernel heaps.

  • Tracked Object Pool (`objpool`): Every allocation is registered in a tracking pool associated with the processing_module.

  • Automatic Garbage Collection on Teardown: When an audio stream closes, the Module Adapter calls mod_free_all(). Even if a third-party algorithm neglects to free internal scratch buffers during its .free() callback, the adapter reclaims every registered memory block automatically, completely preventing memory leaks.