Audio Engine
The oveRTOS audio engine is a graph-based processing framework. Audio flows through a directed acyclic graph (DAG) of typed nodes, validated at build time and executed in topological order.
Architecture
graph LR
subgraph Graph
direction LR
SRC["Source Node<br/><small>I2S / PDM / SDL2</small>"]
PROC["Processor Node<br/><small>DSP / Converter / Gain</small>"]
SINK["Sink Node<br/><small>I2S / SDL2</small>"]
SRC --> PROC --> SINK
end
HW_IN["Hardware Input<br/><small>DMA / Capture</small>"] --> SRC
SINK --> HW_OUT["Hardware Output<br/><small>DMA / Playback</small>"]
style SRC fill:#4a9,stroke:#333,color:#fff
style PROC fill:#48b,stroke:#333,color:#fff
style SINK fill:#a54,stroke:#333,color:#fff
Node types:
| Type | Role | process() contract |
|---|---|---|
| Source | Produces audio from hardware or memory | in = NULL, fills out |
| Processor | Transforms audio (DSP, format conversion, gain) | Reads in, writes out |
| Sink | Consumes audio to hardware or memory | Reads in, out = NULL |
Graph Lifecycle
stateDiagram-v2
[*] --> IDLE: graph_init()
IDLE --> IDLE: add_node() / connect()
IDLE --> READY: build()
READY --> RUNNING: start()
RUNNING --> READY: stop()
READY --> IDLE: deinit()
RUNNING --> RUNNING: process()
- IDLE -- Add nodes and connect them. The graph is mutable.
- build() -- Validates edges, propagates formats via
configure(), resolves topological execution order, allocates buffers. Transitions to READY. - READY -- Graph is immutable. Start begins hardware streaming.
- RUNNING -- Hardware callbacks drive
process()each buffer period.
Execution Modes
Sink-driven (real-time)
The hardware sink's DMA interrupt or callback triggers ove_audio_graph_process(). The engine walks all nodes in topological order within that callback context.
sequenceDiagram
participant DMA as Hardware DMA
participant Engine as Engine Thread
participant Src as Source Node
participant Proc as Processor
participant Sink as Sink Node
DMA->>Engine: buffer ready (ISR notify / callback)
Engine->>Src: process(NULL, &out_buf)
Src-->>Engine: fills output buffer from DMA RX
Engine->>Proc: process(&in_buf, &out_buf)
Proc-->>Engine: transforms audio
Engine->>Sink: process(&in_buf, NULL)
Sink-->>Engine: copies to DMA TX / SDL stream
App-driven (testing / offline)
The application calls ove_audio_graph_process() directly to pump one cycle. No hardware is involved -- useful for unit tests with mock source/sink nodes.
Format Negotiation
Each node declares its output format during build() via the configure() callback:
graph LR
A["Source<br/>configure(NULL, &out_fmt)<br/><small>declares 48kHz/mono/S16</small>"]
B["Converter<br/>configure(in_fmt, &out_fmt)<br/><small>S16 in, declares F32 out</small>"]
C["Sink<br/>configure(in_fmt, NULL)<br/><small>validates F32 input</small>"]
A -->|"48kHz/1ch/S16"| B -->|"48kHz/1ch/F32"| C
Format propagation happens in topological order during build(). If any node rejects its input format, build() returns OVE_ERR_INVALID_PARAM. Connected nodes must agree on sample rate (no built-in resampler).
Built-in Nodes
| Node | Factory | Purpose |
|---|---|---|
| Converter | ove_audio_node_converter() |
Sample format conversion (S16/S32/F32, all combinations) |
| Channel Map | ove_audio_node_channel_map() |
Remap, extract, or silence channels |
| Gain | ove_audio_node_gain() |
Volume control in dB (applies linear gain with clamping) |
| Tap | ove_audio_node_tap() |
Passthrough with observer callback (VU meters, ring buffers) |
Device Nodes (Transports)
Device nodes are hardware-backed sources and sinks created via factory functions:
int ove_audio_device_source(struct ove_audio_graph *g,
const struct ove_audio_device_cfg *cfg,
const char *name);
int ove_audio_device_sink(struct ove_audio_graph *g,
const struct ove_audio_device_cfg *cfg,
const char *name);
| Transport | Source | Sink | Backend |
|---|---|---|---|
OVE_AUDIO_TRANSPORT_I2S |
Yes | Yes | FreeRTOS (STM32 SAI/DMA), Zephyr (device tree), NuttX (/dev/audio) |
OVE_AUDIO_TRANSPORT_PDM |
Yes | No | FreeRTOS (STM32 DFSDM), Zephyr |
OVE_AUDIO_TRANSPORT_SDL2 |
Yes | Yes | POSIX only (host development) |
Custom Nodes
Implement the ove_audio_node_ops vtable to create custom processing nodes:
static int my_dsp_configure(void *ctx, const struct ove_audio_fmt *in,
struct ove_audio_fmt *out)
{
*out = *in; /* same format as input */
return OVE_OK;
}
static int my_dsp_process(void *ctx, const struct ove_audio_buf *in,
struct ove_audio_buf *out)
{
const int16_t *src = (const int16_t *)in->data;
int16_t *dst = (int16_t *)out->data;
/* ... transform audio ... */
return OVE_OK;
}
static const struct ove_audio_node_ops my_dsp_ops = {
.configure = my_dsp_configure,
.process = my_dsp_process,
};
Register it with:
int node = ove_audio_graph_add_node(&graph, &my_dsp_ops, my_ctx,
"my-dsp", OVE_AUDIO_NODE_PROCESSOR);
Example: Guitar IR Convolution
The hIRoic app uses the graph engine for real-time cabinet impulse response convolution:
graph LR
I2S_IN["I2S Source<br/><small>Line-in, 44.1kHz mono</small>"]
DSP["hIRoic DSP Node<br/><small>FFT convolution +<br/>bypass + peak tracking</small>"]
I2S_OUT["I2S Sink<br/><small>Headphone output</small>"]
I2S_IN --> DSP --> I2S_OUT
style I2S_IN fill:#4a9,stroke:#333,color:#fff
style DSP fill:#48b,stroke:#333,color:#fff
style I2S_OUT fill:#a54,stroke:#333,color:#fff
struct ove_audio_device_cfg dev_cfg = {
.transport = OVE_AUDIO_TRANSPORT_I2S,
.fmt = { .sample_rate = 44100, .channels = 1,
.sample_fmt = OVE_AUDIO_FMT_S16 },
};
ove_audio_graph_init(&graph, 512);
int src = ove_audio_device_source(&graph, &dev_cfg, "i2s-in");
int dsp = ove_audio_graph_add_node(&graph, &hiroic_dsp_ops,
NULL, "dsp", OVE_AUDIO_NODE_PROCESSOR);
int sink = ove_audio_device_sink(&graph, &dev_cfg, "i2s-out");
ove_audio_graph_connect(&graph, src, dsp);
ove_audio_graph_connect(&graph, dsp, sink);
ove_audio_graph_build(&graph);
ove_audio_graph_start(&graph);
Example: Keyword Detection with DMIC
The example_keyword_live app extracts DMIC audio from a TDM stream, feeds a ring buffer for ML inference, and passes audio to headphones:
graph LR
DMIC["I2S Source<br/><small>DMIC, 16kHz, 4-slot TDM</small>"]
PROC["DMIC Processor<br/><small>Extract slot 1 (left mic)<br/>Feed ring buffer<br/>Passthrough to headphones</small>"]
HP["I2S Sink<br/><small>Headphone output</small>"]
DMIC --> PROC --> HP
PROC -.->|"ring buffer"| INFER["Inference Thread<br/><small>Audio features + classifier</small>"]
style DMIC fill:#4a9,stroke:#333,color:#fff
style PROC fill:#48b,stroke:#333,color:#fff
style HP fill:#a54,stroke:#333,color:#fff
style INFER fill:#666,stroke:#333,color:#fff
Diagnostics
Query runtime statistics with ove_audio_graph_get_stats():
| Field | Description |
|---|---|
cycles |
Total completed process cycles |
underruns |
Sink starvation events (e.g., DMA phase skip) |
overruns |
Source overflow events (e.g., short capture read) |
node_errors |
Count of process() failures (output zeroed on error) |
max_process_us |
Worst-case cycle time in microseconds |
avg_process_us |
Rolling average cycle time in microseconds |
Timing fields are populated when CONFIG_OVE_TIME is enabled.
Kconfig Options
| Option | Default | Description |
|---|---|---|
CONFIG_OVE_AUDIO |
y |
Enable the audio subsystem |
CONFIG_OVE_AUDIO_NODE_CONVERTER |
y |
Format converter node (S16/S32/F32) |
CONFIG_OVE_AUDIO_NODE_GAIN |
n |
Gain/volume node |
CONFIG_OVE_AUDIO_NODE_TAP |
n |
Observer/tap node |
CONFIG_OVE_AUDIO_NODE_CHANNEL_MAP |
n |
Channel remapping node |
Headers
| Header | Contents |
|---|---|
ove/audio.h |
Graph struct, graph API, diagnostics |
ove/audio_node.h |
Format types, buffer type, node vtable, built-in node factories |
ove/audio_device.h |
Transport enum, device config, device node factories |