Expand description
§dsp-process
dsp-process provides small no_std processing traits and composition adapters
for DSP code that needs to stay explicit about state, memory layout, and hot-path
costs.
It was extracted from idsp, where the same
abstractions are used to build fixed-point and floating-point filters for embedded
and real-time signal-processing pipelines. The crate is intended for code that
cares about:
- predictable code generation
no_stdand no allocation- separating immutable configuration from mutable runtime state
- composing filters without hiding the data layout
- sharing one configuration across many lanes or states
The root idsp repository also carries composite examples that show how these
primitives fit together in real DSP graphs.
This README is the crate-level documentation via #![doc = include_str!(...)].
§Mission
The core idea is simple: treat a DSP stage as a tiny object with a process()
method, and make composition cheap enough that it still works in embedded hot
paths.
The crate is deliberately narrower than general stream-processing frameworks. It does not try to model async execution, dynamic graphs, allocation, buffering ownership, or runtime scheduling. It focuses on synchronous sample/slice processing and on layouts that map cleanly to loops the compiler can optimize.
§Core Traits
The four main traits are:
Process: stateful single-input processing,&mut selfInplace: stateful in-place processingSplitProcess: immutable configuration plus separate mutable stateSplitInplace: in-place variant ofSplitProcess
SplitProcess is the distinctive part of the crate. It lets one configuration be
reused across many independent states, which is a good fit for multi-lane DSP,
I/Q processing, polyphase banks, or coefficient sharing in embedded systems.
§Basic Example
use dsp_process::{Process, Split, Offset, Gain};
let mut offset = Split::stateless(Offset(3));
assert_eq!(offset.process(5), 8);
let mut gain = Split::stateless(Gain(4));
assert_eq!(gain.process(5), 20);§Composition Example
Serial composition uses tuples or arrays. Here two stateless stages are combined into one processor:
use dsp_process::{Process, Split, Offset, Gain};
let mut pipeline = (Split::stateless(Offset(3)) * Split::stateless(Gain(4))).minor();
assert_eq!(pipeline.process(5), 32);The tuple/array implementations cover common static compositions without dynamic dispatch or heap allocation.
§Split Configuration and Shared Coefficients
A single filter configuration can be applied to multiple states:
use dsp_process::{Process, Split, Offset};
let mut lanes = Split::stateless(Offset(3)).lanes::<2>();
assert_eq!(lanes.process([1, 2]), [4, 5]);
assert_eq!(lanes.process([10, 20]), [13, 23]);This is one of the main reasons the crate exists: the split form makes sharing configuration explicit instead of forcing each lane to own a full copy.
§Closures and Adapters
FnProcess and FnSplitProcess let closures participate when that is the
clearest representation:
use dsp_process::{Process, FnProcess};
let mut abs = FnProcess(|x: i32| x.abs());
assert_eq!(abs.process(-7), 7);Adapters such as Chunk, ChunkIn, ChunkOut, ChunkInOut,
Interpolator, Decimator, and Map help lift sample processors to chunk
processors and back without changing the underlying stage.
§Layout and Composition Modes
The crate supports several composition styles:
- plain tuples and arrays for straightforward serial composition
Minorfor processor-minor/data-major compositionMajorfor slice processing with explicit intermediate scratchParallelfor parallel branchesLanesfor many states with shared configurationByLanefor explicit lane-major view processing
These are not interchangeable in performance terms. The point is to make the choice explicit.
Minor tends to fit processors with small state and configuration. Major is
for cases where slice processing and explicit intermediate storage improve cache
behavior or register pressure. Lanes and ByLane pair with typed views so
multi-lane locality and vectorization can be expressed explicitly.
§Context in idsp
Inside idsp, these traits are used to express IIR sections, half-band filters,
decimators, interpolators, low-pass stages, and lock-in style pipelines, often on
fixed-point integer data and often with strong embedded constraints. The crate is
therefore biased toward:
- static composition
- small reusable building blocks
- explicit scratch buffers
- low ceremony in
no_std
If your problem is “I have a hot DSP inner loop and want composition without giving up control”, this crate is aimed at that.
§Benefits and Unique Selling Points
Compared with hand-written monolithic loops, dsp-process gives:
- reusable composition primitives instead of copy-pasted loop nests
- explicit split config/state for coefficient sharing
- static dispatch and fixed layouts instead of runtime graph machinery
- adapters for common DSP reshaping tasks such as chunking, interpolation, and decimation
Compared with more general iterator or stream-style APIs, it keeps:
no_stdsupport- no allocation requirement
- layout control as part of the API
- a closer mapping between the public abstraction and the generated loop nest
§Costs and Limitations
This crate is intentionally not beginner-friendly in every corner.
- The API is low level. Callers are expected to understand sample, slice, and view layout.
- The traits are designed for hot paths, so some contracts are preconditions rather than dynamically checked ergonomic errors.
- Static composition means type signatures can become large.
- Some adapters rely on const-generic shape relations that are correct but not always obvious at the callsite.
LanesandByLaneuse explicit views for layout-sensitive view processing, which is more precise but also a more advanced API than ordinary ordinary slice use.
In short: this crate optimizes for control and performance first, convenience second.
§Alternatives
Concisely:
- Hand-written loops: maximal control, minimal reuse. Often best for a single kernel, worse once many variants or compositions need to stay consistent.
- Iterator-heavy style: concise for non-hot code, usually a poor fit when state, aliasing, and exact loop shape matter.
- Dynamic flowgraph runtimes such as GNU Radio or FutureSDR: better when the
graph, scheduling policy, runtime reconfiguration, or heterogeneous execution
are part of the problem. Those frameworks operate at a higher level around
blocks, buffers, message passing, and schedulers;
dsp-processis much lower level and closer to the inner kernels inside such blocks. - Dynamic in-process graph libraries such as
dasp_graph: better when nodes and edges must be edited at runtime.dsp-processinstead assumes fixed topology and avoids graph ownership and scheduler concerns entirely. - Static signal or graph composition libraries such as
dasp_signalor FunDSP: those also support static composition, but they focus on frame/signal or audio-node abstractions.dsp-processis narrower: split config/state, explicit view layout,no_std, and predictable loop shape are the center of the design. - Plain
Process-only designs: simpler surface, but weaker support for coefficient sharing across many states.
dsp-process is most useful in the middle ground: fixed topology, static
composition, explicit state, and performance-sensitive DSP.
§Notes on Lanes and ByLane
Lanes and ByLane are layout tools, not just semantic conveniences. Their
layout-sensitive view behavior is explicit through
View<_, _, LaneMajor, _> and ViewMut<_, _, LaneMajor, _>, so the
lane-major interpretation is visible at the type level instead of being
silently inferred from [[X; N]].
use dsp_process::{LaneMajor, Offset, Split, View, ViewMut, ViewProcess};
let mut p = Split::stateless(Offset(3)).lanes::<2>();
let x = View::<_, LaneMajor, 2>::from_flat(&[1, 2, 3, 10, 20, 30], 3);
let mut y = [0; 6];
let yb = ViewMut::<_, LaneMajor, 2>::from_flat(&mut y, 3);
ViewProcess::process_view(&mut p, x, yb);
assert_eq!(y, [4, 5, 6, 13, 23, 33]);Chunk adapters remain useful alongside typed views. Use Split::per_frame()
and then call process_frames() or inplace_frames() on the resulting
processor to apply a chunk processor frame by frame to a frame-major view.
§Guidance for Implementors
As a rule of thumb:
- implement
SplitProcess<X, Y, ()>for stateless/config-only processors - implement
Processfor processors that carry all state internally - implement
SplitInplaceorInplacewhen a true in-place specialization exists - override
block()only when it meaningfully improves the loop shape or memory traffic
§Small Reference Example
use dsp_process::{Buffer, Inplace, Process};
let mut dly = Buffer::<[i32; 2]>::default();
let y: i32 = dly.process(10);
assert_eq!(y, 0);
let y: i32 = dly.process(20);
assert_eq!(y, 0);
let y: i32 = dly.process(30);
assert_eq!(y, 10);
let mut block = [1, 2, 3];
dly.inplace(&mut block);
assert_eq!(block, [20, 30, 1]);§API Parameterization Rules
dsp-process uses a small set of parameterization rules throughout the API:
- Use runtime fields for per-instance values that may reasonably vary without changing the type, such as gains, offsets, clamps, held samples, and downsample counts.
- Use const generics for structural facts that define shape, topology, or fixed ratios at compile time, such as lane counts, chunk widths, input/output ratios, slot indices, and view layouts.
- When a structural parameter also determines stored memory shape, encode it
in the field type itself, typically with arrays like
[T; N], rather than duplicating it as both a runtime field and a const generic. - Use wrapper newtypes for semantic adaptation and composition order
(
Chunk,Decimator,Lanes,Minor,Major,ByLane) rather than for user-tunable configuration. - Use stream adapters with explicit state when phase matters across time
(
Downsample,Hold,Decimator,Interpolator); use structural adapters when only per-call shape changes (Rate,ChunkIn,ChunkOut,ChunkInOut, view layout markers).
In short: values live at runtime, shapes live in types, and composition semantics live in wrappers.
Structs§
- Add
- Summation
- Buffer
- Fixed-size sample buffer used as a delay line or chunk accumulator.
- Butterfly
- Sum and difference of a two-element input.
- ByLane
- Explicit lane-major view interpretation for parallel compositions.
- Chunk
- Elementwise fixed-size chunk lifting.
- ChunkIn
- Fixed-ratio chunk adapter for grouped input.
- Chunk
InOut - General fixed-ratio regrouping adapter for chunked input and output.
- Chunk
Out - Fixed-ratio chunk adapter for grouped output.
- Chunk
OutPod - POD-specialized
ChunkOutvariant. - Clamp
- Clamp between min and max using
Ord - Comb
- Comb (derivative)
- Decimator
- Adapt a scalar optional-output stage to chunk input mode.
- Downsample
- Scalar downsampler with explicit tick phase.
- FnProcess
- Wrap a
FnMutinto aProcess/Inplace - FnSplit
Process - Wrap a
Fninto aSplitProcess/SplitInplace - Frame
Major - Frame-major view layout marker.
- Gain
- Multiplication by a constant in split form.
- Hold
- Zero-order hold over optional input samples.
- Identity
- Identity stage and simple fan-out/fan-in adapter.
- Integrator
- Running sum / discrete-time integrator.
- Interpolator
- Adapt a scalar optional-input stage to chunk output mode.
- Lane
Major - Lane-major view layout marker.
- Lanes
- Multiple lanes with one shared configuration and separate states.
- Major
- Stage-major slice composition with explicit scratch storage.
- Map
- Lift a processor through
OptionorResult. - Minor
- Processor-minor, data-major serial composition.
- Mul
- Product
- Neg
- Inversion using
Neg. - Nyquist
- Nyquist zero with gain 2
- Offset
- Addition of a constant in split form.
- Parallel
- Parallel branch composition over tuple or array-shaped data.
- PerFrame
- Apply a chunk-based processor frame by frame to a frame-major view.
- Rate
- Select or place one sample in a fixed-size rate-conversion slot.
- Split
- A stateful processor assembled from split configuration and state.
- Sub
- Difference
- TryDecimator
- Checked variant of
Decimator. - Unsplit
- Marker for values that should live in the opposite half of a
Split. - View
- Immutable typed view of a DSP slice.
- ViewMut
- Mutable typed view of a DSP slice.
Enums§
- Decimator
Error - Error returned by
TryDecimatorwhen the inner decimator does not tick exactly once per input chunk.
Traits§
- Inplace
- Inplace processing
- Process
- Processing block
- Split
Inplace - Inplace processing with a split state
- Split
Process - Processing with split state
- Split
View Inplace - Split-state in-place processing API over typed views.
- Split
View Process - Split-state processing API over typed views.
- View
Inplace - Explicit in-place processing API over typed views.
- View
Process - Explicit processing API over typed views.
Type Aliases§
- Pair
- Parallel filter pair