<alp/dsp.h> — Signal Processing Chains
Composable FIR / IIR / window / FFT chains run against in-RAM sample buffers, over CMSIS-DSP where available with a portable-C fallback everywhere else. The header also ships one-pass summary statistics and an RBJ biquad designer.
Build a chain with alp_dsp_chain_open() against a list of alp_dsp_stage_t descriptors, then feed sample buffers through it.
Header
#include <alp/dsp.h>
Build a filter-terminated chain
static const float fir_taps[8] = { /* ... */ };
alp_dsp_stage_t stages[] = {
{ .kind = ALP_DSP_STAGE_FIR,
.u.fir = { .coeff_format = ALP_DSP_COEFF_FORMAT_F32,
.n_taps = 8,
.taps = fir_taps } },
};
alp_dsp_chain_t *c = alp_dsp_chain_open(stages, 1u);
int16_t out[256];
size_t got = 0;
alp_dsp_chain_apply_samples(c, in_mv, 256u, out, 256u, &got);
alp_dsp_chain_close(c);
alp_dsp_chain_open() copies the stage params — including the coefficient arrays — into the chain, so the caller's source memory can be freed immediately on return.
Build an FFT-terminated chain
alp_dsp_stage_t stages[] = {
{ .kind = ALP_DSP_STAGE_WINDOW,
.u.window = { .shape = ALP_DSP_WINDOW_HANN } },
{ .kind = ALP_DSP_STAGE_FFT,
.u.fft = { .n_points = 128u,
.output_format = ALP_DSP_FFT_OUTPUT_MAGNITUDE_ONESIDED } },
};
alp_dsp_chain_t *c = alp_dsp_chain_open(stages, 2u);
float mag[128 / 2 + 1]; // one-sided: n_points/2 + 1 bins
size_t got = 0;
alp_dsp_chain_apply_bins(c, in_mv, 128u, mag, sizeof(mag) / sizeof(mag[0]), &got);
alp_dsp_chain_close(c);
Chain validation rules
Enforced at alp_dsp_chain_open():
- 1..
ALP_DSP_MAX_STAGESstages. - At most one
ALP_DSP_STAGE_FFT, and if present it must be the terminal stage. - A
ALP_DSP_STAGE_WINDOW(if present) must immediately precede the FFT. A window without a terminating FFT is rejected — it has no defined meaning in the filtered-samples path. - Per-stage param ranges are bounded (see the limits below).
Limits
| Macro | Value | Bounds |
|---|---|---|
ALP_DSP_MAX_STAGES | 4 | Stages per chain. |
ALP_DSP_MAX_FIR_TAPS | 64 | Taps per FIR stage. |
ALP_DSP_MAX_IIR_SECTIONS | 8 | Biquad sections per IIR stage (cascaded DF1). |
ALP_DSP_MIN_FFT_POINTS | 32 | Minimum FFT size (power-of-two). |
ALP_DSP_MAX_FFT_POINTS | 1024 | Maximum FFT size (power-of-two). |
Stage kinds
| Kind | Params | Notes |
|---|---|---|
ALP_DSP_STAGE_FIR | u.fir | coeff_format, n_taps (1..64), taps. |
ALP_DSP_STAGE_IIR | u.iir | coeff_format, n_sections (1..8), coeffs — 5 * n_sections entries, ordered b0, b1, b2, a1, a2 per section. Each section computes y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]. |
ALP_DSP_STAGE_WINDOW | u.window | shape: RECTANGULAR / HANN / HAMMING / BLACKMAN. The window length is bound to the following FFT's n_points; coefficients are computed inside the chain at open time. |
ALP_DSP_STAGE_FFT | u.fft | n_points (power-of-two in range), output_format. |
FFT output formats
| Format | Output element count | Notes |
|---|---|---|
ALP_DSP_FFT_OUTPUT_COMPLEX | 2 * n_points | Interleaved (re, im) f32 pairs. Bins are not normalised. |
ALP_DSP_FFT_OUTPUT_MAGNITUDE | n_points | Per-bin magnitude sqrt(re² + im²), full two-sided spectrum. |
ALP_DSP_FFT_OUTPUT_MAGNITUDE_ONESIDED | n_points/2 + 1 | Positive-frequency half only — bins 0..N/2, DC through Nyquist. The natural output of a real FFT (the two-sided spectrum is just this mirrored), so prefer it for real-input spectra: half the output buffer, no redundant negative-frequency bins. |
Coefficient formats
| Format | Caller pointer type | Notes |
|---|---|---|
ALP_DSP_COEFF_FORMAT_F32 | const float * | IEEE-754 single precision. Valid for FIR and IIR. |
ALP_DSP_COEFF_FORMAT_Q31 | const int32_t * | Q31 fixed-point (full-scale = ±1.0). Valid for FIR only. |
:::caution Q31 is rejected on IIR stages
An IIR stage with ALP_DSP_COEFF_FORMAT_Q31 makes alp_dsp_chain_open() fail with ALP_ERR_NOSUPPORT. Q31 maps full-scale to ±1.0, but a biquad's a1 coefficient ranges to ±2 — a direct Q31→f32 map would silently wrap every real IIR coefficient set into the wrong filter. Rather than ship a subtly-wrong filter, the SDK rejects it: pass F32 coefficients, or design them with alp_dsp_biquad_design(). Q31 stays valid for FIR, whose taps are bounded to ±1.
:::
Applying a chain
Four apply calls: two for filter-terminated chains, two for FFT-terminated chains, each in an int16-millivolt flavour and a float-native flavour.
| Call | Use when |
|---|---|
alp_dsp_chain_apply_samples(chain, in_mv, in_n, out_mv, out_cap, got) | Chain does not contain an FFT. int16_t millivolt domain — composes with <alp/adc.h>. |
alp_dsp_chain_apply_samples_f32(chain, in, in_n, out, out_cap, got) | Same, but consumes and produces float directly — no int16 quantisation on either edge. Prefer for float pipelines. |
alp_dsp_chain_apply_bins(chain, in_mv, in_n, out_bins, out_cap, got) | Chain is FFT-terminated. int16_t millivolt input. |
alp_dsp_chain_apply_bins_f32(chain, in, in_n, out_bins, out_cap, got) | Same, but float input. Prefer for float pipelines. |
All four write the number of output elements to *got. Output may be in-place (out == in).
Calling an apply_samples* variant on an FFT-terminated chain returns ALP_ERR_NOSUPPORT (use apply_bins*), and vice versa.
Streaming semantics
Pre-FFT FIR / IIR stages carry their filter state across calls (a sliding filter), while the FFT itself consumes exactly n_points samples from the input each call. Feeding successive — optionally overlapping — windows therefore yields an STFT-style stream. alp_dsp_chain_close() resets all filter state.
Summary statistics
alp_dsp_stats_f32() computes the mean, RMS, population variance, min/max, and peak magnitude (plus its index) of a real float buffer in one pass:
alp_dsp_stats_t st;
if (alp_dsp_stats_f32(samples, n, &st) == ALP_OK) {
printf("rms=%f peak=%f @ %u\n", st.rms, st.abs_max, st.abs_max_index);
}
| Field | Type | Notes |
|---|---|---|
mean | float | Arithmetic mean, (1/n) * Σ x[i]. |
rms | float | Root-mean-square, sqrt((1/n) * Σ x[i]²). |
variance | float | Population variance, E[x²] - E[x]² (≥ 0). |
min | float | Minimum sample value. |
max | float | Maximum sample value. |
abs_max | float | Maximum magnitude (the peak) over all samples. |
abs_max_index | uint32_t | Index of the abs_max sample. |
Returns ALP_OK, or ALP_ERR_INVAL when x or out is NULL or n is 0 (in which case out is left untouched).
- Population, not sample, variance.
variancedivides byn, notn - 1— it is not what CMSIS-DSP'sarm_var_f32returns. The value is identical on the CMSIS and portable paths. - All fields describe the buffer verbatim. For AC statistics (DC/mean removed), mean-centre the buffer yourself and pass the centred data.
- The backend runs
arm_mean_f32/arm_rms_f32/arm_min_f32/arm_max_f32/arm_absmax_f32on Cortex-M when the cmsis-dsp module is linked, and a single portable-C pass otherwise — so application code never callsarm_*directly and the same source builds on every target.
Biquad designer
alp_dsp_biquad_design() computes the five normalised coefficients for a single ALP_DSP_STAGE_IIR section from a frequency and Q, so callers do not hand-derive filter math:
float coeffs[5];
// 2nd-order Butterworth low-pass at 1 kHz, fs = 16 kHz
alp_dsp_biquad_design(ALP_DSP_BIQUAD_LOWPASS, 1000.0f, 16000.0f, 0.70710678f, coeffs);
alp_dsp_stage_t stages[] = {
{ .kind = ALP_DSP_STAGE_IIR,
.u.iir = { .coeff_format = ALP_DSP_COEFF_FORMAT_F32,
.n_sections = 1,
.coeffs = coeffs } },
};
| Parameter | Notes |
|---|---|
kind | ALP_DSP_BIQUAD_LOWPASS / ALP_DSP_BIQUAD_HIGHPASS / ALP_DSP_BIQUAD_BANDPASS (constant 0 dB peak) / ALP_DSP_BIQUAD_NOTCH (band-reject). |
f0_hz | Cutoff (LP/HP) or centre (BP/notch) frequency. Must satisfy 0 < f0_hz < fs_hz/2. |
fs_hz | Sample rate in Hz; must be > 0. |
q | Quality factor; must be > 0. Butterworth LP/HP uses 1/sqrt(2) ≈ 0.70710678; higher Q = narrower / peakier. |
coeffs_out | Receives { b0, b1, b2, a1, a2 } normalised (a0 = 1), ready to pass as an IIR stage's coeffs with n_sections = 1 and ALP_DSP_COEFF_FORMAT_F32. |
Returns ALP_OK, or ALP_ERR_INVAL on a NULL pointer, a bad kind, or a frequency/Q outside the valid ranges.
:::note Scope boundary
This covers only the four RBJ audio-EQ-cookbook second-order responses — deliberately, for maintenance. It is not a general filter-design toolbox: higher-order Butterworth cascades, Chebyshev / elliptic / Bessel families, and arbitrary pole placement (Parks-McClellan etc.) are out of scope on purpose, and belong in application code or an external design tool feeding F32 coefficients to ALP_DSP_STAGE_IIR.
:::
Errors
alp_dsp_chain_open() returns NULL with alp_last_error() set to:
| Code | Cause |
|---|---|
ALP_ERR_INVAL | NULL pointer, zero stage count, bad enum value, or a chain-ordering violation (FFT not terminal, window not preceding FFT, …). |
ALP_ERR_OUT_OF_RANGE | Per-stage bound violation (n_taps / n_sections / n_points outside the limits, or n_points not a power-of-two). |
ALP_ERR_NOMEM | The static chain pool is exhausted (compile-time pool size). |
ALP_ERR_NOSUPPORT | Q31 coefficients on an IIR stage (see above). |
Backends
| Backend | Notes |
|---|---|
| CMSIS-DSP | Preferred, when ALP_HAS_CMSIS_DSP=1 — arm_fir_*, arm_biquad_cascade_df1_*, arm_rfft_fast_f32. |
| Portable C | Fallback otherwise — naive convolution + radix-2 Cooley-Tukey, i.e. O(N·M) / O(N²). Fine for unit tests and small (≲ 256-point) chains; not suitable for the hot path. |
List cmsis-dsp in board.yaml's libraries: to get the CMSIS-DSP kernels:
libraries:
- cmsis-dsp
Applications that want spectral or filtered ADC data at line rate should target a SoM with a hardware backend (V2N family today; AEN once the wave-2 bridge ships).
alp_dsp_chain_capabilities(chain) returns the backend-populated capability descriptor (HW-FFT-present, Q31 fast-path, …); NULL → NULL.
ABI status
[ABI-EXPERIMENTAL] — the chain surface arrived in v0.5 and composes with <alp/adc.h>'s filter/spectrum types; both sides may co-evolve. v0.10 adds alp_dsp_stats_f32, the *_f32 apply variants, ALP_DSP_FFT_OUTPUT_MAGNITUDE_ONESIDED, and alp_dsp_biquad_design (all additive), and makes Q31-on-IIR an explicit ALP_ERR_NOSUPPORT.
See also
<alp/adc.h>— the ADC-domain source these chains compose with<alp/audio.h><alp/ahrs.h>— sensor fusion<alp/pid.h>— control loops