Skip to main content

<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.

#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_STAGES stages.
  • 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

MacroValueBounds
ALP_DSP_MAX_STAGES4Stages per chain.
ALP_DSP_MAX_FIR_TAPS64Taps per FIR stage.
ALP_DSP_MAX_IIR_SECTIONS8Biquad sections per IIR stage (cascaded DF1).
ALP_DSP_MIN_FFT_POINTS32Minimum FFT size (power-of-two).
ALP_DSP_MAX_FFT_POINTS1024Maximum FFT size (power-of-two).

Stage kinds

KindParamsNotes
ALP_DSP_STAGE_FIRu.fircoeff_format, n_taps (1..64), taps.
ALP_DSP_STAGE_IIRu.iircoeff_format, n_sections (1..8), coeffs5 * 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_WINDOWu.windowshape: 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_FFTu.fftn_points (power-of-two in range), output_format.

FFT output formats

FormatOutput element countNotes
ALP_DSP_FFT_OUTPUT_COMPLEX2 * n_pointsInterleaved (re, im) f32 pairs. Bins are not normalised.
ALP_DSP_FFT_OUTPUT_MAGNITUDEn_pointsPer-bin magnitude sqrt(re² + im²), full two-sided spectrum.
ALP_DSP_FFT_OUTPUT_MAGNITUDE_ONESIDEDn_points/2 + 1Positive-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

FormatCaller pointer typeNotes
ALP_DSP_COEFF_FORMAT_F32const float *IEEE-754 single precision. Valid for FIR and IIR.
ALP_DSP_COEFF_FORMAT_Q31const 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.

CallUse 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);
}
FieldTypeNotes
meanfloatArithmetic mean, (1/n) * Σ x[i].
rmsfloatRoot-mean-square, sqrt((1/n) * Σ x[i]²).
variancefloatPopulation variance, E[x²] - E[x]² (≥ 0).
minfloatMinimum sample value.
maxfloatMaximum sample value.
abs_maxfloatMaximum magnitude (the peak) over all samples.
abs_max_indexuint32_tIndex 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. variance divides by n, not n - 1 — it is not what CMSIS-DSP's arm_var_f32 returns. 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_f32 on Cortex-M when the cmsis-dsp module is linked, and a single portable-C pass otherwise — so application code never calls arm_* 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 } },
};
ParameterNotes
kindALP_DSP_BIQUAD_LOWPASS / ALP_DSP_BIQUAD_HIGHPASS / ALP_DSP_BIQUAD_BANDPASS (constant 0 dB peak) / ALP_DSP_BIQUAD_NOTCH (band-reject).
f0_hzCutoff (LP/HP) or centre (BP/notch) frequency. Must satisfy 0 < f0_hz < fs_hz/2.
fs_hzSample rate in Hz; must be > 0.
qQuality factor; must be > 0. Butterworth LP/HP uses 1/sqrt(2)0.70710678; higher Q = narrower / peakier.
coeffs_outReceives { 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:

CodeCause
ALP_ERR_INVALNULL pointer, zero stage count, bad enum value, or a chain-ordering violation (FFT not terminal, window not preceding FFT, …).
ALP_ERR_OUT_OF_RANGEPer-stage bound violation (n_taps / n_sections / n_points outside the limits, or n_points not a power-of-two).
ALP_ERR_NOMEMThe static chain pool is exhausted (compile-time pool size).
ALP_ERR_NOSUPPORTQ31 coefficients on an IIR stage (see above).

Backends

BackendNotes
CMSIS-DSPPreferred, when ALP_HAS_CMSIS_DSP=1arm_fir_*, arm_biquad_cascade_df1_*, arm_rfft_fast_f32.
Portable CFallback 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, …); NULLNULL.

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

Questions about this page? Discuss in Community Forum