This repository is a digital ladder-filter evaluation framework. It benchmarks multiple implementations against a shared SPICE reference of the Moog ladder filter under matched operating conditions.
The core idea is simple: linear agreement is useful, but it is not enough. The largest differences between ladder-filter implementations usually appear under nonlinear operation: harmonic structure, large-signal cutoff shift, self-oscillation behaviour, and saturation placement.
nonlinear_cutoff_sweep.mp4
nonlinear_drive_sweep.mp4
Requirements:
- CMake 3.22 or later
- C++20 compiler (Clang or GCC)
- Python 3.9 or later
- ngspice — required for SPICE simulation
Windows users should use WSL2 with Ubuntu. The commands in this README assume a Unix-like shell environment, and native Windows setup is not the primary target.
Install ngspice:
# macOS (Homebrew)
brew install ngspice
# Ubuntu / Debian
sudo apt install ngspiceFetch the bundled model submodules, configure, build, and generate a representative plot:
git submodule update --init --recursive
cmake -S . -B build
cmake --build build
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e .
testbench-run nonlinear_harmonic_spectra --models spice huovilainen2004 zavalishin2012This writes figures to build/results/<case_name>/Figure_*.png.
If you want to compare only bundled digital models without ngspice:
testbench-run nonlinear_harmonic_spectra --models huovilainen2004 zavalishin2012 dangelo2014For run modes (all, individual cases, audio), see
Usage.
For full setup context (including optional external models), see
Setup and Build.
The scope is deliberately narrow. This repository focuses on ladder behaviour: linear frequency characteristics, self-oscillation, large-signal cutoff shift, and harmonic structure. CPU cost, UI behaviour, host automation, audio-rate parameter smoothing, and product-level readiness are outside scope.
Typical use is:
- add or integrate one digital ladder-filter model
- run the existing case set against the shared SPICE reference
- compare the new model against the existing baselines with the same plots and metrics
The included model set spans research references, embedded DSP libraries, synthesis languages, DSP frameworks, and widely used open-source music applications.
The repository combines:
- C++ model runners for digital implementations
- ngspice-based analog reference measurement
- Python orchestration for case execution and normalization
- shared plotting and reporting over per-case CSV outputs
The main entry point is testbench-run, which executes one case or a group of
cases for a chosen model set and writes results under build/results/.
For runtime cost measurement, the repository also provides targets/:
targets/host_bench- desktop microbenchmark target for quick relative comparisons
targets/pico2- RP2350 benchmark target that emits CSV over UART/USB CDC
The targets flow is intentionally separate from the SPICE-quality workflow:
testbench/ is for behavioural comparison, while targets/ is for CPU-budget
measurement.
The digital ladder-filters compared here include not only implementations with an explicit ladder structure, but also implementations that approximate the behaviour of the Moog ladder filter as deployed in real-world open-source music software ecosystems.
For consistency, some integrations apply analog-domain I/O scaling at the boundary when the underlying implementation expects a different internal signal scale than the SPICE reference. This keeps the compared models in a shared operating domain without changing their internal DSP structure.
| Model | Algorithm / Source | Nonlinear placement | Saturator | OS | Variants | Bundled |
|---|---|---|---|---|---|---|
| SPICE | Authentic BJT ladder (ngspice) | 1+4 (BJT differential pairs) | BJT | 8× | — | yes |
| Huovilainen 2004 | "Non-linear digital implementation of the Moog ladder filter" | 1+4 | tanh | 2× | yes | yes |
| D'Angelo 2013 | "An improved virtual analog model of the Moog ladder filter" | 1+4 | tanh | 1x | — | yes |
| D'Angelo 2014 | "Generalized Moog ladder filter, Part II" | 1+4 | tanh | 1x | — | yes |
| Zavalishin 2012 | "The Art of VA Filter Design" | 1+4 | tanh | 1x | yes | yes |
| Mystran 2012 | "Cheap non-linear zero-delay filters" | 1+4 | Padé-approx tanh | 1x | yes | yes |
| FAUST | ve.moogLadder — linear 4-pole TPT/ZDF |
none | — | 1x | — | yes |
| Csound | moogladder opcode |
1+4 | tanh | 2× | — | no (LGPL) |
| SuperCollider | MoogFF — linear 4-pole |
none | — | 1x | — | no (GPL) |
| Pure Data | bob~ — RK4 ODE (Puckette, 2015) |
1+4 | cubic clip | 2× | — | yes |
| JUCE | dsp::LadderFilter |
boundary — sat(in) + sat(fb) | tanh LUT | 1x | — | no (GPL / commercial) |
| Surge XT | VintageLadder — RK4 ODE, zero-stuffed input | 1+4 | cubic clip | 4× | yes | no (GPL) |
| VCV Rack | Fundamental VCF — RK4 ODE, Padé-approx tanh |
1+4 | Padé-approx tanh | 1x | — | no (GPL) |
| DaisySP | LadderFilter — Huovilainen New Moog |
boundary — sat(in + fb) | Padé-approx tanh | 4× | — | yes |
| Teensy | AudioFilterLadder — Huovilainen New Moog |
boundary — sat(in + fb) | Padé-approx tanh | 4× | — | yes |
Each case is designed to examine one aspect of linear-domain frequency behaviour, self-oscillation behaviour, or nonlinear behaviour and timbre.
| Case | What it measures |
|---|---|
linear_frequency_response |
Frequency response via ESS deconvolution (k=2) |
linear_cutoff_accuracy |
Measured cutoff frequency accuracy from the ESS-derived response (upper −3 dB crossing) vs nominal cutoff parameter |
linear_resonance_gain |
Q factor of the resonance peak from the ESS-derived response |
self_oscillation_sustain |
Sustained self-oscillation at a fixed high-resonance evaluation point (k=4.3) |
nonlinear_cutoff_shift |
Cutoff frequency shift under large-signal input |
nonlinear_harmonic_spectra |
Harmonic spectra via Farina (2000) ESS deconvolution |
nonlinear_drive_sweep |
Harmonic evolution vs input amplitude and resonance |
nonlinear_cutoff_sweep |
Harmonic evolution during a high-resonance cut-off sweep |
Activate the virtual environment before calling Python entry points:
# macOS / Linux
source .venv/bin/activate
# Windows
.venv\Scripts\activateThe primary entry point is testbench-run. It drives both ngspice-based
analog reference measurements and C++ digital ladder-filter implementations
through a single Python interface, then generates a plot from the selected
outputs.
The nonlinear drive sweep figure above compares spice,
huovilainen2004, csound, faust, and vcvrack so the kinds of
deviations surfaced by the benchmark are visible at a glance.
To regenerate selected models for one case and plot only those models:
testbench-run nonlinear_cutoff_shift --models spice dangelo2014 mystran2012To run the same comparison set across every case:
testbench-run all --models spice huovilainen2004 zavalishin2012Per-model sampling-rate overrides use the model@rate form:
testbench-run self_oscillation_sustain --models spice teensy@44100 mystran2012Case-specific options are exposed directly on the unified CLI:
testbench-run self_oscillation_sustain \
--models spice mystran2012 \
--cutoff 1200 --input-freq 1000Use testbench-run --list to see the available case names. If needed, lower-level
execution paths can be inspected in testbench.plot, testbench.spice, and
build/testbench.
To add a new digital ladder-filter implementation, see
models/README.md.
The targets/ directory provides runtime-oriented benchmarks that complement
the quality-focused testbench/ plots.
For RP2350 benchmarking:
PICO_SDK_PATH=/path/to/pico-sdk cmake -S targets/pico2 -B build-target-pico2
cmake --build build-target-pico2This produces build-target-pico2/pico_bench.uf2. Flash it to the board,
capture the UART or USB CDC CSV output, and save the log for plotting. A
practical convention is:
build/results/benchmark_cpu_occupancy/rp2350.csv
Then render the benchmark figure with:
python -m testbench.plot benchmark_cpu_occupancy \
--input build/results/benchmark_cpu_occupancy/rp2350.csvThe benchmark CSV includes both cycles/sample and derived Kernel CPU Occupancy (%). The intended primary hardware target is RP2350 with
copy_to_ram binaries.
testbench-audio generates WAV files that let you hear the timbral changes
that the nonlinear harmonic evolution plots show visually.
Listen to build/audio/nonlinear_drive_sweep/huovilainen2004.wav or
build/audio/nonlinear_drive_sweep/mystran2012.wav to hear how the same
C2 sawtooth morphs as each model's saturation structure activates.
Available audio cases:
nonlinear_drive_sweepnonlinear_cutoff_sweep
Each WAV contains five amplitude levels of a sawtooth input (0.001–0.390 V, linearly spaced in the SPICE voltage domain) passed through the ladder filter. The fundamental (H1) is held constant across segments so that harmonic redistribution caused by saturation is audible as pure timbre change.
Default parameters match the nonlinear_drive_sweep test case:
C2 (65.41 Hz) sawtooth, cutoff 1000 Hz, resonance k = 0, 96 kHz (44100 Hz for teensy).
# Generate audio demos for nonlinear_drive_sweep
testbench-audio nonlinear_drive_sweep --models spice huovilainen2004 dangelo2014 zavalishin2012 mystran2012
# Specific models only
testbench-audio nonlinear_drive_sweep --models huovilainen2004 mystran2012
# Generate the resonant cut-off sweep audio demo
testbench-audio nonlinear_cutoff_sweep --models spice daisy mystran2012
# Force regeneration (including CSVs)
testbench-audio nonlinear_drive_sweep --forceWAV files are written under build/audio/<case>/.
Clone the repository with its submodules:
git clone --recursive https://github.com/oyama/ladder-filter-testbench.git
cd ladder-filter-testbenchThe bundled Teensy, DaisySP, and Pure Data models are Git submodules, so the
--recursive flag is required. If you already cloned without it, run
git submodule update --init --recursive.
Optional external models (listed in Models) can be added by cloning
the corresponding model repository under models/<name>/ before running CMake.
Build the testbench, then create a Python environment with the interpreter of your choice and install the tools (editable):
cmake -S . -B build
cmake --build build
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e .Then use the commands in Usage.
The metrics below summarize the patterns described above. They are intended as comparative error measures against the SPICE reference, not as universal perceptual thresholds.
In quantitative terms, the linear-domain differences are modest while the
nonlinear-domain differences are much larger. In one representative mixed-model
comparison, the default models cluster at 0.116–0.363 dB in linear response
summary error, but their nonlinear summaries already separate clearly in
H1-normalized linear units:
| Model | Linear (dB) | Spectra (% H1) | Sweep (% H1) | Level (% H1) |
|---|---|---|---|---|
huovilainen2004 |
0.116 | 0.04 | 0.17 | 0.05 |
dangelo2014 |
0.363 | 0.06 | 0.22 | 0.18 |
Spectra, Sweep, and Level are averages of H2–H9 RMSE in linear
amplitude after H1 normalization, reported as percent of H1. These values are
representative rather than exhaustive. Full per-case results are in the
Metrics Report.
The shared resonance parameter k is defined at the benchmark boundary, with
k ~= 4 intended as a nominal onset convention: models should approach the
self-oscillation boundary in that region under the common interface.
The self_oscillation_sustain case itself is evaluated at k=4.3, because
oscillation onset and sustained oscillation amplitude are not the same
observable. The higher setting gives a more stable comparison point once
oscillation is already present.
These remappings are observational calibrations at the model boundary. They do not modify the internal state update, nonlinearity placement, or feedback topology of the integrated models.
A summary report of quantitative metrics across all test cases can be generated with:
source .venv/bin/activate
testbench-report --build-dir build --output build/results/report.mdThe report is written to build/results/report.md as a Markdown table.
Each test case reports one or more numerical metrics comparing each model against the SPICE reference:
| Case | Metrics |
|---|---|
linear_frequency_response |
Magnitude bias and std vs SPICE (dB) — passband + transition band |
linear_cutoff_accuracy |
Cutoff frequency RMSE and max error vs SPICE (cents) |
linear_resonance_gain |
Q factor RMSE and max error vs SPICE |
self_oscillation_sustain |
RMS amplitude in the final 0.5 s after excitation ends (mV) |
nonlinear_cutoff_shift |
Resonance peak frequency RMSE and max error vs SPICE (cents) |
nonlinear_harmonic_spectra |
Per-harmonic (H2–H9) RMSE vs SPICE (linear level, H1-normalized) |
nonlinear_cutoff_sweep |
Per-harmonic (H2–H9) RMSE vs SPICE over swept cutoff (linear level, H1-normalized) |
nonlinear_drive_sweep |
Per-harmonic (H2–H9) RMSE vs SPICE at k=0 (linear level, H1-normalized) |
SPICE reference accuracy.
The SPICE simulation serves as the reference for comparison throughout this
testbench.
Its accuracy depends on the fidelity of the component models used.
The BJT parameters used here are generic SPICE defaults, not measurements
taken from a specific physical unit.
Temperature dependence of the thermal voltage (V_T = kT/q), component tolerances,
and higher-order device effects such as the Early effect are not modelled.
No comparison against physical hardware has been performed.
The SPICE reference should therefore be understood as a simulation-based reference
rather than a ground truth derived from a real instrument.
Sampling rate. Each test case runs at a fixed sampling rate chosen to make the behaviour being measured clearly visible. Some cases deliberately use a lower rate than a production context would typically require, because differences between models become more pronounced at lower rates. Results obtained at one sampling rate do not necessarily predict behaviour at another. Users evaluating a model for a specific application should run the relevant test cases at the sampling rate that application requires.
-
A. Farina, "Simultaneous Measurement of Impulse Response and Distortion with a Swept-Sine Technique," in 108th AES Convention, Paris, France, 2000, AES Paper 5093.
-
A. Huovilainen, "Non-Linear Digital Implementation of the Moog Ladder Filter," in Proceedings of the International Conference on Digital Audio Effects (DAFx), Naples, Italy, 2004.
-
V. Zavalishin, The Art of VA Filter Design, 2012. Native Instruments white paper. URL: https://www.native-instruments.com/fileadmin/ni_media/downloads/pdf/VAFilterDesign_2.1.2.pdf
-
S. D'Angelo and V. Välimäki, "An Improved Virtual Analog Model of the Moog Ladder Filter," in Proc. IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP), Vancouver, Canada, 2013, pp. 729–733. DOI: 10.1109/ICASSP.2013.6637744
-
S. D'Angelo and V. Välimäki, "Generalized Moog Ladder Filter: Part II---Explicit Nonlinear Model through a Novel Delay-Free Loop Implementation Method," IEEE/ACM Transactions on Audio, Speech, and Language Processing, vol. 22, no. 12, pp. 1873–1883, 2014. DOI: 10.1109/TASLP.2014.2352556
-
T. Voipio (mystran), "Cheap Non-Linear Zero-Delay Filters", KVR Audio Forum, 2012. URL: https://www.kvraudio.com/forum/viewtopic.php?t=349859
-
Teensy Audio Library. URL: https://github.com/PaulStoffregen/Audio
-
DaisySP DSP Library. URL: https://github.com/electro-smith/DaisySP
-
ngspice Circuit Simulator. URL: https://ngspice.sourceforge.io/
-
M. Puckette, Pure Data source tree,
extra/bob~/bob~.c, 2015. URL: https://github.com/pure-data/pure-data