Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/scripts/ci_ffi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Build the C ABI and compile/run a C smoke test against the shipped header.
#
# `embedded-dsp-ffi` is a standalone workspace (see its Cargo.toml), so it is
# addressed by manifest path rather than as a root-workspace member, and its
# artifacts are redirected under the main `target/` directory.
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"

manifest="crates/embedded-dsp-ffi/Cargo.toml"
export CARGO_TARGET_DIR="${EDS_FFI_TARGET_DIR:-$repo_root/target/ffi}"
out_dir="$CARGO_TARGET_DIR/debug"

cargo clippy --manifest-path "$manifest" --all-targets -- -D warnings
cargo build --manifest-path "$manifest"

# The C compiler is the check: the test uses every exported symbol through the
# public header, so a Rust/header signature drift fails to compile or link.
cc -std=c11 -Wall -Wextra -Werror -pedantic \
crates/embedded-dsp-ffi/tests/c_smoke.c \
-I crates/embedded-dsp-ffi/include \
-L "$out_dir" -lembedded_dsp_ffi \
-Wl,-rpath,"$out_dir" \
-lm -o "$out_dir/eds_c_smoke"

"$out_dir/eds_c_smoke"
17 changes: 17 additions & 0 deletions .github/scripts/ci_fuzz.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Bounded differential fuzzing of the FIR and biquad kernels.
#
# The targets compare the `f32` kernels against in-process `f64` references, so a
# found input is a real numerical divergence, not just a crash. Bounded so the
# job stays well under a minute; run longer locally with FUZZ_RUNS.
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root/fuzz"

cargo fuzz build

for target in fir_differential biquad_differential; do
echo "fuzzing: $target"
cargo fuzz run "$target" -- -runs="${FUZZ_RUNS:-20000}" -max_len=4096
done
26 changes: 26 additions & 0 deletions .github/scripts/ci_miri.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Miri undefined-behaviour check.
#
# Two unit tests are skipped, for reasons unrelated to this crate:
# * `nalgebra_interop` reaches libm's x86 `sqrtss` inline assembly, which Miri
# cannot execute ("inline assembly is not supported").
# * `compile_time_atan_helpers_and_private_divi` asserts last-bit `atan`
# accuracy against the native libm; Miri's float shims are not bit-identical.
# Everything else (the 67 remaining unit tests) plus the fixed-point and
# elementwise integration suites run under Miri.
#
# Set MIRI_CARGO to a toolchain-qualified cargo (e.g. "cargo +nightly-2026-07-25")
# to run against a specific nightly locally.
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"

cargo_bin="${MIRI_CARGO:-cargo}"
export MIRIFLAGS="${MIRIFLAGS:--Zmiri-disable-isolation}"

$cargo_bin miri test -p embedded-dsp --all-features --lib -- \
--skip nalgebra_interop \
--skip compile_time_atan_helpers_and_private_divi
$cargo_bin miri test -p embedded-dsp --all-features --test fixed_basic_types
$cargo_bin miri test -p embedded-dsp --all-features --test basic_math
19 changes: 19 additions & 0 deletions .github/scripts/ci_python.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Build the PyO3 extension and run the Python smoke test against it.
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"

manifest="crates/embedded-dsp-py/Cargo.toml"
export CARGO_TARGET_DIR="${EDS_PY_TARGET_DIR:-$repo_root/target/py}"

cargo build --manifest-path "$manifest"

# Import the cdylib under the name the module's init symbol expects.
stage="$(mktemp -d)"
cp "$CARGO_TARGET_DIR/debug/libembedded_dsp.so" "$stage/embedded_dsp.so"

PYTHONPATH="$stage" python3 crates/embedded-dsp-py/tests/smoke.py

rm -rf "$stage"
65 changes: 64 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,28 @@ jobs:
echo "::endgroup::"
done

hack:
name: Feature powerset (cargo-hack)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-hack
- uses: Swatinem/rust-cache@v2
# `--features libm` keeps the always-on base (the crate requires `std` or
# `libm`), then every feature is checked individually on top of it.
- run: cargo hack check -p embedded-dsp --each-feature --no-dev-deps --features libm

machete:
name: Unused dependencies (cargo-machete)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo install cargo-machete --locked
- run: cargo machete

test:
name: Test
runs-on: ubuntu-latest
Expand Down Expand Up @@ -174,10 +196,51 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: bash .github/scripts/ci_docs.sh

ffi:
name: C ABI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: bash .github/scripts/ci_ffi.sh

python:
name: Python bindings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- uses: Swatinem/rust-cache@v2
- run: bash .github/scripts/ci_python.sh

miri:
name: Miri (UB)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@nightly
with:
components: miri, rust-src
- uses: Swatinem/rust-cache@v2
- run: bash .github/scripts/ci_miri.sh

fuzz:
name: Fuzz (bounded)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
- run: cargo install cargo-fuzz --locked
- run: bash .github/scripts/ci_fuzz.sh

publish-dry-run:
name: Publish dry-run
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/mutants.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Mutants

# Weekly mutation testing over the always-compiled core. Informational: it runs on
# a schedule, never on pull requests.
#
# `cargo-mutants` discovers modules from the `mod` declarations it can see. This
# crate declares its feature modules through the `gated_mod!` macro, which it
# cannot follow, so only `math`, `types`, and `intrinsics` are mutable today;
# widen `--file` once discovery covers the feature modules.

on:
schedule:
- cron: "17 5 * * 1" # Mondays, 05:17 UTC
workflow_dispatch:

permissions:
contents: read

jobs:
mutants:
name: Mutation testing (shard ${{ matrix.shard }})
runs-on: ubuntu-latest
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
shard: ["1/8", "2/8", "3/8", "4/8", "5/8", "6/8", "7/8", "8/8"]
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo install cargo-mutants --locked
- name: Mutate
run: |
cargo mutants -p embedded-dsp \
--file crates/embedded-dsp/src/math.rs \
--file crates/embedded-dsp/src/types.rs \
--file crates/embedded-dsp/src/intrinsics.rs \
--shard "${{ matrix.shard }}" \
--timeout 120
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
All notable changes to this project are documented in this file. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]
## [0.6.0] - 2026-09-13

### Added

- **C ABI** (`embedded-dsp-ffi`): a standalone `staticlib`/`cdylib` exposing FIR, Direct Form I biquad cascades, the complex FFT, and the audio-EQ designer to C and C++, with a hand-maintained header and a C smoke test that compiles, links, and runs against it in CI.
- **Python bindings** (`embedded-dsp-py`): a PyO3 stable-ABI (`abi3-py39`) extension module (`embedded_dsp`) exposing the audio-EQ designer, FIR, and biquad cascades, with maturin wheel metadata.
- **Verification CI**: a Miri undefined-behaviour job, bounded `cargo-fuzz` differential targets (`fuzz/`, `f32` against in-process `f64`) backed by an always-on randomized differential test, a scheduled sharded `cargo-mutants` run over the always-compiled core, `cargo-hack` per-feature builds, and `cargo-machete`.
- **Unified audio-EQ builder, `IHo`, and WebAudio export** (`filter_design`): `EqFilter` + `EqShape` + `BiquadType` design every RBJ Audio EQ Cookbook response from one fluent, validating builder — including the `IHo` (integrator-over-harmonic-oscillator) section `idsp` had and this crate lacked — and `WebAudioFilter` mirrors a `BiquadFilterNode`'s `type`/`frequency`/`detune`/`Q`/`gain`. Coefficients are bit-checked against `idsp`'s `iir::coefficients` across all nine response types.

- **RBJ EQ parameter conversions**: `biquad_q_from_bw` and `biquad_q_from_shelf_slope` turn an octave bandwidth or a shelf slope into the `q` the biquad designers take. The bandwidth relation is bilinear-transform corrected, so a band keeps its octave width as the centre approaches Nyquist. Also corrected the `biquad_bandpass_coeffs` doc, which named the wrong variant.
- **Swept-sine inverse filter**: `Sweep::inverse_filter` deconvolves an exponential sweep in one complex multiply per bin, recovering an impulse response at `t = 0`. Matches `idsp` 0.22.1.
- **Hyperbolic CORDIC**: `cordic_sqrt_atanh2_q31` and `cordic_atanh_q31` add the hyperbolic vectoring mode, within ~`6e-9` of `f64` across the representable domain. The `cosh`/`sinh` rotation and the linear modes are deliberately not ported; see the README note.
Expand Down Expand Up @@ -39,6 +44,25 @@ All notable changes to this project are documented in this file. The format foll
- **LMS/NLMS and the recursive moving average are generic over `DspSample`**: `LmsInstance`/`NlmsInstance` (built on `AdaptiveSample`) and `RecursiveMovingAverage<T, N>` replace four width twins, again keeping the old names as aliases/wrappers. The q15 paths are bit-exact with the kernels they replace; `RecursiveMovingAverage::<N>` becomes `RecursiveMovingAverage::<f32, N>`.
- **One composition vocabulary (`pipeline`)**: `Process`/`Inplace` are now blanket-derived from `SplitProcess`/`SplitInplace` — a stateless stage implements `SplitProcess<X, Y, ()>` once and inherits `Process` (and, through the second blanket, `DspNode`) — so `Split`, `Chain`, `Gain`, `Limiter`, `Offset`, `Identity`, `Buffer`, `SinglePoleFilter<T>`, `PidInstanceF32/Q15`, and `DcBlockerQ15` lost their hand-written bridges. The split vocabulary's receivers became `&mut self` and its methods were renamed to keep method resolution unambiguous: `SplitProcess::process`/`block` → `process_with_state`/`block_with_state`, `SplitInplace::inplace` → `inplace_with_state`, `SplitViewProcess::process_view` → `process_view_with_state`, `SplitViewInplace::inplace_view` → `inplace_view_with_state`. `DspNode::process_block`'s shorter-buffer clamp and the specialised `Buffer`/`ChunkInOut` block/in-place paths are preserved.

### Removed

- **The duplicate `filtering::Dsm` and `filtering::XorShift32` are gone.** They shadowed the dedicated `dsm::Dsm` / `dither::XorShift32` modules, so only one of each could be re-exported at the crate root. The dedicated modules are now the single implementation, re-exported at the crate root as before, and `filtering` re-exports neither name. `dsm::Dsm` is `Default` + `process()` + `reset()` — the historical `new()`/`process_sample()` names are deliberately **not** carried over. `dither::XorShift32` gains the `next_f32()` / `tpdf_dither_f32()` helpers that only the removed duplicate had.

- **The sample-genericization compatibility layer is removed.** The width-suffixed instance aliases (`FirInstanceF32/Q31/Q15`, `BiquadCascadeInstanceF32/Q15/Q31`, `BiquadCascadeDf2tInstanceF32/Q15/Q31`, `LmsInstanceF32/Q15`, `NlmsInstanceF32/Q15`, `PidInstanceF32/Q31/Q15`, `HilbertTransformF32/Q15`, `RecursiveMovingAverageQ15`) and their thin width wrappers (`fir_f32/q31/q15`, `biquad_cascade_df1_*`, `biquad_cascade_df2t_*`, `lms_*`, `lms_leaky_*`, `nlms_*`, `pid_f32/q31/q15`) are gone. Call the generic API directly instead: `FirInstance<f32>`, `BiquadCascadeInstance<q15>`, `LmsInstance<f32>`, `HilbertTransform<'_, f32>`, `RecursiveMovingAverage<q15, N>`, and the free functions `fir` / `biquad_cascade_df1` / `biquad_cascade_df2t` / `lms` / `lms_leaky` / `nlms`; PID updates are `PidInstance::<T>::process`.

### Changed

- **`filtering` is split into family submodules** (`fir`, `biquad`, `convolution`, `adaptive`, `recursive`, `lockin`, `int_filters`, `normal_form`, `wdf`) behind a re-exporting facade; `embedded_dsp::filtering::*` and the crate-root glob are unchanged. The integration-test suite is likewise consolidated from 46 files into 36 domain-named files, with every test and its `required-features` preserved.
- **docs.rs shows feature-gate badges** for every module (`#[cfg_attr(docsrs, doc(cfg(...)))]`).

## [0.5.1] - 2026-09-06

### Changed

- **The `fixed` dependency is now optional**, pulled in only by the fixed-point features; default builds no longer compile it.
- **Dependency bumps**: `defmt` 0.3 → 1.1, plus Dependabot action updates.
- **CI**: Codecov coverage, a `cargo-deny` audit, Dependabot, and a release workflow.

## [0.5.0] - 2026-09-01

### Added
Expand Down
14 changes: 7 additions & 7 deletions COOKBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Execute high-frequency (20–50 kHz) current loop control in pure Q15 or floatin
use embedded_dsp::*;

// Setup controller states
let mut id_pid = PidInstanceQ15::new(8000, 2000, 0); // D-axis flux PID
let mut iq_pid = PidInstanceQ15::new(12000, 3000, 0); // Q-axis torque PID
let mut id_pid = PidInstance::<q15>::new(8000, 2000, 0); // D-axis flux PID
let mut iq_pid = PidInstance::<q15>::new(12000, 3000, 0); // Q-axis torque PID

// ADC current measurements (Phase A, B, C) in Q15 format
let i_a: q15 = 12000;
Expand All @@ -46,8 +46,8 @@ park_q15(i_alpha, i_beta, sin_theta, cos_theta, &mut i_d, &mut i_q);
// 4. Current Loop PID Regulators
let target_i_d: q15 = 0; // Zero d-axis current (maximum torque per ampere)
let target_i_q: q15 = 15000; // Commanded torque
let v_d = pid_q15(&mut id_pid, target_i_d.saturating_sub(i_d));
let v_q = pid_q15(&mut iq_pid, target_i_q.saturating_sub(i_q));
let v_d = id_pid.process(target_i_d.saturating_sub(i_d));
let v_q = iq_pid.process(target_i_q.saturating_sub(i_q));

// 5. Inverse Park Transform (rotating dq -> stationary αβ voltage commands)
let mut v_alpha: q15 = 0;
Expand Down Expand Up @@ -77,15 +77,15 @@ let post_shift = biquad_quantize_and_scale_q15(

// 3. Initialise DMA block processor
let mut biquad_state = [0i16; 4];
let mut biquad = BiquadCascadeInstanceQ15::init(1, &q15_coeffs, &mut biquad_state, post_shift);
let mut biquad = BiquadCascadeInstance::<q15>::with_post_shift(1, &q15_coeffs, &mut biquad_state, post_shift);
let mut dc_blocker = DcBlockerQ15::new(32112); // R ≈ 0.98

// In DMA callback / audio loop
fn process_dma_buffer(
dma_rx: &[q15],
dma_tx: &mut [q15],
dc_blocker: &mut DcBlockerQ15,
biquad: &mut BiquadCascadeInstanceQ15,
biquad: &mut BiquadCascadeInstance<'_, q15>,
) {
let mut temp = [0i16; 64];
let len = dma_rx.len().min(temp.len());
Expand All @@ -96,7 +96,7 @@ fn process_dma_buffer(
}

// Step B: Biquad Filtering
biquad_cascade_df1_q15(biquad, &temp[..len], &mut dma_tx[..len]);
biquad_cascade_df1(biquad, &temp[..len], &mut dma_tx[..len]);
}
```

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ members = [
"crates/embedded-dsp",
"crates/embedded-dsp-studio",
]
# Standalone link targets, built by their own CI jobs and kept out of
# `--workspace` builds: `fuzz` needs a nightly toolchain, and the C ABI must not
# inherit `embedded-dsp`'s `defmt` feature into its cdylib link.
exclude = ["fuzz", "crates/embedded-dsp-ffi"]

[workspace.package]
version = "0.5.1"
version = "0.6.0"
edition = "2024"
rust-version = "1.93"
authors = ["Gerzain Mata <leftger@gmail.com>"]
Expand All @@ -16,6 +20,11 @@ readme = "README.md"

[workspace.lints.rust]
unsafe_code = "deny"
# `docsrs` is set by `[package.metadata.docs.rs].rustdoc-args` (see the crate
# manifest) so `#[cfg_attr(docsrs, doc(cfg(...)))]` adds feature-gate badges on
# docs.rs only. Declaring the custom cfg keeps `unexpected_cfgs` quiet under
# `-D warnings` on stable, where the cfg is never set.
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(docsrs)"] }

[workspace.lints.clippy]
# The DSP kernels are written as index loops over parallel arrays; many index
Expand All @@ -25,7 +34,7 @@ unsafe_code = "deny"
needless_range_loop = "allow"

[workspace.dependencies]
embedded-dsp = { path = "crates/embedded-dsp", version = "0.5.1", default-features = false }
embedded-dsp = { path = "crates/embedded-dsp", version = "0.6.0", default-features = false }
fixed = { version = "1.31.0", default-features = false }
libm = { version = "0.2.11" }
defmt = { version = "1.1", default-features = false }
Expand Down
Loading