Skip to content

Repository files navigation

Moog Ladder Filter Testbench

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.

Demo

nonlinear_cutoff_sweep.mp4
nonlinear_drive_sweep.mp4

Quick Start

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 ngspice

Fetch 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 zavalishin2012

This 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 dangelo2014

For run modes (all, individual cases, audio), see Usage. For full setup context (including optional external models), see Setup and Build.

Overview

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.

Models

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 yes
Huovilainen 2004 "Non-linear digital implementation of the Moog ladder filter" 1+4 tanh 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 no (LGPL)
SuperCollider MoogFF — linear 4-pole none 1x no (GPL)
Pure Data bob~ — RK4 ODE (Puckette, 2015) 1+4 cubic clip 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 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 yes
Teensy AudioFilterLadder — Huovilainen New Moog boundary — sat(in + fb) Padé-approx tanh yes

Test Cases

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

Usage

Activate the virtual environment before calling Python entry points:

# macOS / Linux
source .venv/bin/activate

# Windows
.venv\Scripts\activate

Plots

The 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.

Figure_nonlinear_drive_sweep

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 mystran2012

To run the same comparison set across every case:

testbench-run all --models spice huovilainen2004 zavalishin2012

Per-model sampling-rate overrides use the model@rate form:

testbench-run self_oscillation_sustain --models spice teensy@44100 mystran2012

Case-specific options are exposed directly on the unified CLI:

testbench-run self_oscillation_sustain \
    --models spice mystran2012 \
    --cutoff 1200  --input-freq 1000

Use 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.

Runtime Benchmarks

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-pico2

This 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.csv

The benchmark CSV includes both cycles/sample and derived Kernel CPU Occupancy (%). The intended primary hardware target is RP2350 with copy_to_ram binaries.

Audio

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_sweep
  • nonlinear_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 --force

WAV files are written under build/audio/<case>/.

Setup and Build

Clone the repository with its submodules:

git clone --recursive https://github.com/oyama/ladder-filter-testbench.git
cd ladder-filter-testbench

The 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.

Representative Metrics

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.

Resonance Convention Notes

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.

Quantitative Metrics Report

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.md

The 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)

Limitations

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.

References

  1. A. Farina, "Simultaneous Measurement of Impulse Response and Distortion with a Swept-Sine Technique," in 108th AES Convention, Paris, France, 2000, AES Paper 5093.

  2. 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.

  3. 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

  4. 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

  5. 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

  6. T. Voipio (mystran), "Cheap Non-Linear Zero-Delay Filters", KVR Audio Forum, 2012. URL: https://www.kvraudio.com/forum/viewtopic.php?t=349859

  7. Teensy Audio Library. URL: https://github.com/PaulStoffregen/Audio

  8. DaisySP DSP Library. URL: https://github.com/electro-smith/DaisySP

  9. ngspice Circuit Simulator. URL: https://ngspice.sourceforge.io/

  10. M. Puckette, Pure Data source tree, extra/bob~/bob~.c, 2015. URL: https://github.com/pure-data/pure-data

About

A quantitative evaluation testbench for digital ladder filters against a SPICE reference

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages