# iir1, a header-only realtime IIR filter library for C++

> berndporr/iir1 implements Butterworth, RBJ and Chebyshev filters for realtime sample-by-sample processing, with memory allocated at compile time so no malloc or new ever runs in the filter path. Coefficients can also come from scipy, and the project ships CMake packages and Ubuntu PPA builds.

**berndporr/iir1** — DSP IIR realtime filter library written in C++

- Repository: https://github.com/berndporr/iir1
- Website: http://berndporr.github.io/iir1/
- Stars: 778 · Forks: 162
- Language: C++
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/berndporr-iir1

## Why the templates exist: no allocation in the audio path

iir1 is an infinite impulse response filter library for Linux, Mac OS X and Windows. The README lists high performance and realtime sample in, sample out processing as the headline features, and then explains the mechanism that delivers them.

The filters are template based and header only, and the template argument is the filter order. Instantiating a fourth order Butterworth lowpass looks like this:

```cpp
Iir::Butterworth::LowPass<order> f;
```

Because the order is a compile time constant, the memory the filter needs is allocated at compile time. The README states the consequence directly: the library runs without any malloc or new commands, so there is never the risk of a memory leak, and all realtime filter code lives in the header files so the compiler can optimise the filter and the calling program together.

That is the whole argument for the design, and it is a real one. In a realtime audio callback, allocation is a source of glitches, and a library that structurally cannot allocate removes the question. The cost is that the order is baked into the type, so a runtime-selectable order means several instantiations, and the README notes that the order given to the template is the default used by setup but can be overridden by a lower order.

All realtime filter code being in headers also means integration is a copy of files rather than a link step, which suits projects that vendor their dependencies and dislike binary artefacts in the build.

## Filter families and the two ways to specify frequencies

Four families are documented, all available as lowpass, highpass, bandpass and bandstop or notch variants. Butterworth and Chebyshev additionally offer low and band shelves with a specified passband gain and zero decibel gain in the stopband.

Butterworth, in Butterworth.h, is described as the standard filter suitable for most applications, with a monotonic response. Chebyshev Type I in ChebyshevI.h permits passband ripple in decibels, and Chebyshev Type II in ChebyshevII.h gives the worst permissible stopband rejection in decibels. RBJ in RBJ.h is the second order design with a cutoff and a Q factor.

The frequency convention is the detail that causes bugs. The library offers two setup paths. One takes the sampling rate and analogue cutoff frequencies, and the other, named setupN, takes normalised frequencies between 0 and 1/2 where 1/2 is the Nyquist frequency. Internally the library works in normalised frequencies, and the setup commands simply divide by the sampling rate when given analogue values. Normalised frequencies are f divided by Fs, in units of one per sample.

So the two forms of a lowpass setup look like this:

```cpp
const int order = 4; // 4th order (=2 biquads)
Iir::Butterworth::LowPass<order> f;
const float samplingrate = 1000; // Hz
const float cutoff_frequency = 5; // Hz
f.setup (samplingrate, cutoff_frequency);
```

and the normalised variant is a single call:

```cpp
f.setupN(norm_cutoff_frequency);
```

The RBJ variant adds the Q factor to both forms:

```cpp
Iir::RBJ::LowPass f;
const float cutoff_frequency = 100;
const float Q_factor = 5;
f.setup (samplingrate, cutoff_frequency, Q_factor);
```

## Running samples through and how invalid input is handled

Processing is one sample at a time. A value goes in, a value comes out:

```cpp
y = f.filter(x);
```

The README notes that the types of x and y can be float or double, and that integer input is also allowed but is still processed internally as floating point. The call is then repeated for every incoming sample in a loop or an event handler.

Error handling has two layers. Invalid values passed to setup throw an exception, and so do parameters that produce NaN coefficients. This second check is the more useful one, because a cutoff above the Nyquist frequency or a nonsensical Q factor produces a filter that will silently produce garbage samples rather than crash, and the exception converts that into a visible failure at configuration time.

If an exception is unacceptable in your context, the library can be built with IIR1_NO_EXCEPTIONS defined via CMake or in the program itself, which switches the checks off. That is a build-time switch rather than a runtime one, so you choose at compile time whether configuration errors are fatal.

There is no built-in buffering or block processing in the documented interface. The API is scalar, one call per sample, which keeps the integration trivial and lets the compiler unroll a loop, at the cost of a call per sample.

## Importing scipy designs through the custom cascade

Not every useful filter is one of the four built-in families, and the library handles that with Custom.h, which takes a second order section cascade straight from Python. The README shows the workflow: run scipy signal design, print the coefficients, then paste them into a std::array of arrays, replacing the braces as the comment says.

The Python side uses signal.ellip with an output of sos:

```python
from scipy import signal
order = 4
sos = signal.ellip(order, 5, 40, 0.2, 'low', output='sos')
print(sos) # copy/paste the coefficients over & replace [] with {}
```

and the C++ side declares the coefficients as a std::array of two std::array of six doubles, which is the standard second order section layout of b0 through b2 and a0 through a2, then constructs an Iir::Custom::SOSCascade from it.

This is the escape hatch for people who design filters in Python, and the repository ships the scripts to match. The demo directory contains elliptic_design.py and bessel_design.py, and plot_impulse_fresponse.py for checking the result, alongside ecg50hzfilt.cpp which filters a recorded ECG and plot_ecg_filt.py which draws the output. A Bessel design has no built-in family in the library, which is exactly the kind of case the custom cascade exists for.

One caveat the README does not spell out but the design implies: copying coefficients by hand is error prone, and there is no runtime check that the pasted numbers correspond to the filter you think you designed. The demo scripts exist to catch that.

## Linking, packages and building from source

The library is header only for the filter code, but the project also builds shared and static libraries through CMake. For a CMake consumer the README gives two blocks, one for the dynamic library and one for the static one:

```cmake
find_package(iir)
target_link_libraries(... iir::iir)
```

The static variant uses the iir::iir_static target, and the generic linker route names the artefacts directly: -liir on Unix and Mac, iir.lib on Windows, libiir_static.a and libiir_static.lib for the static library.

For Ubuntu LTS users the README points at a personal package archive:

```bash
sudo add-apt-repository ppa:berndporr/dsp
```

After that, sudo apt install libiir1 installs the library files and sudo apt install libiir-dev the development files, with documentation and example programs under /usr/share/doc/libiir1-dev/. The archive covers 64 bit Intel and 32 and 64 bit ARM, which includes Raspberry Pi boards.

Building from source uses CMake, which the README says is available for Linux, Windows and Mac and also compiles directly on a Raspberry Pi. On Linux and Mac the sequence is:

```bash
cmake .
make
sudo make install
```

The repository top level holds a CMakeLists.txt, a Config.cmake.in for the package config, a Doxyfile for documentation, an iir.pc.in for pkg-config, a .clang-format configuration and a JoinPaths.cmake helper. The demo/ directory has its own CMakeLists.txt, so the examples build separately from the library.

On release, the version numbering is plain: 1.10.0 is described as a bugfix release with a new dynamic library version, 1.9.5 as a bugfix release, and 1.9.4 as a copy constructor bugfix. The last push was on 2026-10-19, so the source is being worked on more recently than the last tag.

## What iir1 does not cover, and how it compares to a full DSP framework

The scope is a filter, and only a filter. There is no audio file reading or writing, no device handling, no real time scheduling, no block processing and no visualisation. The demo programs exist in the repository and are built separately, which keeps the distinction clear: the library is the part you ship, the demos are the part you read.

That narrower scope is the main reason to prefer it over a full framework. A DSP framework typically bundles filtering, transforms, resampling, file handling and a graph or signal framework, and pulls in a dependency tree with its own build requirements. iir1 is a set of headers with four filter families and a custom cascade. For an embedded or realtime application that already handles its own audio, that is exactly the right size.

The alternative worth weighing is writing the biquads yourself. A Butterworth of fourth order is two cascaded biquads, and most DSP references show the coefficient maths in full. Doing it by hand means you own the code and can change it, but you also own the corner cases the library has already handled: normalised frequency conversion, NaN detection on invalid parameters, and the second order section layout that scipy produces.

There is also the scipy route without any C++ filter at all, since a Python pipeline can do elliptic, Butterworth and Chebyshev design directly and apply it with signal.sosfilt. That is the natural choice for offline analysis and for research code. iir1 earns its place when the filter has to run inside a realtime C++ program, where calling into Python is not an option.

## Conclusion

Adopt iir1 if you are writing realtime signal processing in C++ and want Butterworth, RBJ or Chebyshev filters without a runtime allocation in the audio path, and you are comfortable compiling headers per filter order. Skip it if you need a library that also provides file I/O, real time guarantees or a graph interface, because this one only offers the filter itself. Verify first by adding the include of Iir.h, calling setup with your sampling rate and cutoff, and confirming the response in your own harness before committing to it in a realtime path.

## FAQ

### How do I create and configure a filter with iir1?

Instantiate the filter with its order as a template argument, for example Iir::Butterworth::LowPass<order>, then call setup with the sampling rate and cutoff frequency, or setupN with a normalised frequency between 0 and 1/2. Process each sample with y = f.filter(x).

### Does iir1 allocate memory at runtime?

No. The order is a compile time template argument, so the filter memory is allocated at compile time. The README states the library runs without any malloc or new commands, so there is no risk of a memory leak.

### Which filter designs does iir1 provide?

Butterworth, Chebyshev Type I, Chebyshev Type II and RBJ, each available as lowpass, highpass, bandpass and bandstop or notch. Butterworth and Chebyshev also offer low and band shelves with a specified passband gain and 0 dB gain in the stopband.

### How do I use filter coefficients designed in Python scipy with iir1?

Design the filter in Python with scipy signal output in sos form, print the coefficients and paste them into a std::array of second order sections, replacing the braces. The README shows the elliptic_design.py and bessel_design.py scripts in the demo directory doing exactly this for a custom cascade.

### How is iir1 installed and linked on Linux?

You can add the PPA with sudo add-apt-repository ppa:berndporr/dsp and then install libiir1 and libiir-dev, or build from source with cmake ., make and sudo make install. A CMake consumer uses find_package(iir) and links the iir::iir or iir::iir_static target.

## Sources

- [berndporr/iir1 on GitHub](https://github.com/berndporr/iir1)
- [License: MIT](https://github.com/berndporr/iir1/blob/master/LICENSE)
- [Project website](http://berndporr.github.io/iir1/)
- [README](https://github.com/berndporr/iir1/blob/master/README.md)
- [Releases](https://github.com/berndporr/iir1/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/berndporr-iir1
