diff --git a/README.md b/README.md index 577a5a6..4aa42fc 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ A high-performance **`#![no_std]` Rust Digital Signal Processing library** desig ## Highlights - **`#![no_std]` First**: Pure `core` compatibility with zero heap allocations. -- **Fixed & Float Parity**: CMSIS-style `f32`, `f64`, and `q7`/`q15`/`q31` — interoperable with the [`fixed`](https://crates.io/crates/fixed) crate (optional `fixed` feature, enabled by default) with zero-dependency fallback newtypes — plus the polymorphic `DspSample` trait. +- **Fixed & Float Parity**: CMSIS-style `f32`, `f64`, and `q7`/`q15`/`q31` — interoperable with the [`fixed`](https://crates.io/crates/fixed) crate (optional `fixed` feature, enabled by default) with zero-dependency fallback newtypes — plus the polymorphic `DspSample` trait. Filters, PID, Hilbert transform, CFFT/BFP and complex-math are implemented once as generic `DspSample` code and verified bit-exact against the per-width kernels they replace; the old `F32`/`Q15`/`Q31` names remain as type aliases. - **Hardware Acceleration**: ARM Cortex-M assembly intrinsics (`smlad`, `smlald`, `ssat`, `qadd16`) via `cortex-m-dsp`, with portable SWAR vector fallbacks. - **Pure-Integer CORDIC Engine**: Shift-and-add `sin`, `cos`, `atan2`, polar conversion, and `sqrt` requiring no hardware multipliers. - **Streaming Pipelines**: Zero-allocation [`DspNode`](crates/embedded-dsp/src/pipeline.rs) composable processing chains (`Chain`, `Gain`, `Limiter`). -- **Production Tested**: CI runs the full test suite on the host and `cargo check --all-targets --all-features` plus a `no_std` build on six embedded/WebAssembly targets (`thumbv6m-none-eabi`, `thumbv7em-none-eabi`, `thumbv7em-none-eabihf`, `thumbv8m.main-none-eabihf`, `riscv32imc-unknown-none-elf`, `wasm32-unknown-unknown`), and enforces a **97% line-coverage floor** on the library. +- **Production Tested**: CI runs the full test suite on the host and `cargo check --all-targets --all-features` plus a `no_std` build on six embedded/WebAssembly targets (`thumbv6m-none-eabi`, `thumbv7em-none-eabi`, `thumbv7em-none-eabihf`, `thumbv8m.main-none-eabihf`, `riscv32imc-unknown-none-elf`, `wasm32-unknown-unknown`), and enforces a **96% line-coverage floor** on the library. --- @@ -47,14 +47,12 @@ cargo run -p embedded-dsp-studio ## Comparison with `idsp` -[`idsp`](https://crates.io/crates/idsp) (0.22, by Robert Jördens / QUARTIQ) is the other well-established -`#![no_std]`, integer-first DSP crate; [Stabilizer](https://github.com/quartiq/stabilizer) is its -comprehensive production user. `embedded-dsp` ports and re-verifies `idsp`'s fixed-point and integer -algorithms, so for the algorithms the two share it is a superset — and it adds transforms, audio and -vision, sensor fusion, control, and tooling on top. - -The table is checked against `idsp` `0.22.1`. Honest differences are marked, including the places where -`idsp` still has more to offer; those are also called out below the table. +[`idsp`](https://crates.io/crates/idsp) (0.22, by Robert Jördens / QUARTIQ) is the other established +`#![no_std]`, integer-first DSP crate, used in production by +[Stabilizer](https://github.com/quartiq/stabilizer). `embedded-dsp` ports and re-verifies `idsp`'s +fixed-point/integer algorithms — a superset where they overlap — and adds transforms, audio/vision, +sensor fusion, control, and tooling on top. The table below is checked against `idsp` 0.22.1; honest +gaps are marked and explained after the table. | Feature | `embedded-dsp` | `idsp` | | :--- | :---: | :---: | @@ -96,50 +94,23 @@ The table is checked against `idsp` `0.22.1`. Honest differences are marked, inc Legend: ✅ full support · ➖ partial/alternative coverage · ⚠️ quirk · ❌ not provided. -**Where `idsp` still leads.** It publishes Python bindings for offline analysis and filter design. -Everything else in the table is either at parity or an `embedded-dsp` advantage — including the typed -view framework, the chunk bridges and the gated `Buffer`, which this crate now carries natively -rather than depending on `dsp-process` for. The one piece deliberately not mirrored is `Major`; see -the note below. - -**Note on the extra CORDIC modes.** `idsp` also advertises linear (`mul`/`div`) and hyperbolic -rotation (`cosh_sinh`) modes. Measured against `idsp` 0.22.1, pinned by `tests/idsp_cordic_probe.rs`: - -- `div` — correct (`z + y/x`). Not ported: a fixed-point divide is one instruction here. -- `mul` — correct only for `|z| ≤ 0.5`, folding to `y + x(1 − z)` above it. -- `cosh_sinh` — correct only to `|z| ≈ 0.3`, sign-flipping past `0.5`. - -The band is an upstream bug: the linear table's first entry reads `−1.0` where the angle must be -`+1.0`. Upstream also panics on `idsp::div(i32::MIN, 0, 0)`, since `i32::MIN` is `−1.0` in Q31 and -every vectoring mode negates `x`. Both reported upstream. - -This crate implements hyperbolic **vectoring** instead: both outputs are representable across their -full domains, measure to ~`6e-9`, and have no band or panicking input. Rotation mode is capped by the -format, not the algorithm: `cosh` grows like `e^z`, so Q1.31 reaches `|z| ≈ 0.63` at full scale — -about twice what `idsp` manages. - -**Note on `Major`.** `dsp-process` also ships `Major`, which traverses a composition *stage by stage* -over a whole block, materialising the intermediate signal in explicit scratch, as opposed to the -default `Minor` where one sample walks through every stage. It is not ported, for three reasons: - -- **The composition it needs does not exist here.** Upstream's `Major
` threads scratch between
- stages of a *tuple-seq* composition (`Minor<(C0, C1), U>`). Ours is `Chain`, homogeneous over
- one sample type, so the type-level problem it solves — what is the intermediate type between stage
- *i* and *i+1* — does not arise. Here it is just `&mut [T]`.
-- **The payoff needs stages with real `block` specializations.** Count ours: `Buffer` and
- `ChunkInOut`. Everything else is a recursive per-sample stage — biquads, single-pole, PLL, AGC, DSM
- — where sample-major is already optimal, because keeping that stage's state hot in registers is the
- point.
-- **The capability is already reachable in one line.** `a.block(&x, &mut scratch); b.block(&scratch,
- &mut y);` *is* stage-major traversal. What the upstream type adds is doing it automatically for a
- composed tuple and owning the scratch — neither of which is worth a type parameter that propagates
- through every bound and combinator.
-
-If it is ever built, the shape would be a method taking **caller-owned** scratch rather than a
-`Major