Tests and measurements¶
The tests render signals through the compiled DSP offline and check the results numerically. Nothing needs an audio device or a plugin host, so they run the same way on a laptop and in CI.
make test # every effect
make -j4 test # every effect, four at a time
make test FX=bassdrive # one effect
tests/run.sh bassdrive # the same, once the renderers are built
With -j, each effect's tests run as a separate target (test-<name>), in parallel, and their output is printed one effect at a time. A failing effect doesn't stop the others. test_cpu.py is left out of that parallel pass (tests/run.sh --no-cpu) and runs afterwards for each effect in turn, with nothing else running, because it measures time.
Each script prints [PASS] or [FAIL] lines and exits non-zero if any check fails. The results go to tests/out/<name>/.
The render harness¶
The tests don't use the LV2 bundle. They use a separate renderer, build/<name>/render, built from the effect's .dsp with the Faust architecture file tests/lib/render-arch.cpp:
build/bassdrive/render <samplerate> [options] [Label=value[@seconds] ...] < in.f32 > out.f32
- It reads mono raw float32 samples on stdin and writes the processed samples to stdout.
- Controls are set by their label, for example
Gain=80 Balance=100. Any control not named keeps its declared default. Label=value@secondschanges a control that far into the input, at that exact sample, as host automation does. Repeat a label for a sequence of changes.- Before reading any input it runs two seconds of silence through the DSP, so the parameter smoothers settle on their targets.
--block Nsets the block size (default 256).--latencyprints the latency the effect reports to hosts.--benchtimescompute()on the input instead of writing audio, and--ftzsets the FPU's flush-to-zero first, as the VST3 and CLAP wrappers do.
build/<name>/render_1x is the same DSP with oversample4( replaced by oversample1( and oversample4c( by oversample1c(. Comparing the two shows what the oversampling buys.
Python helpers¶
Each test script imports what it needs from tests/lib/fxtest.py. tests/run.sh sets PYTHONPATH and the FX environment variable, which names the effect under test. Only numpy and matplotlib are needed.
| Name | What it is |
|---|---|
SR |
Sample rate for all tests: 48000 |
RENDER, RENDER_1X |
Paths of the oversampled and 1x renderers |
OUT |
The output directory, tests/out/<name>/ |
render(x, binary=RENDER, events=(), block=None, **params) |
Process signal x, overriding sliders by label: render(x, Gain=100). events is a list of (label, value, seconds) automation points |
reported_latency() |
The latency in samples the effect reports to hosts |
bench(x, block=64, ftz=False, **params) |
Time the DSP on x; returns nanoseconds per sample and the real-time factor |
controls() |
The effect's controls in parameter order (label, type, init, min, max, step), from faust -json |
sine(freq, dur, amp_db=-12) |
A sine of dur seconds at amp_db dBFS peak |
log_sweep(f0, f1, dur, amp_db=-12) |
An exponential sine sweep, plus its instantaneous frequency |
transfer(x, y, nfft=16384) |
H1 transfer-function estimate (Welch-averaged cross-spectrum); returns (f, H) |
harmonics(y, f0, n=10) |
Amplitudes of harmonics 1 to n over the last whole second. f0 must be a whole number of hertz. |
pluck(f0, dur, amp_db=-12, decay=0.5, n=8) |
A synthetic plucked note: harmonics 1 to n at 1/k amplitude, a 1 ms attack and an exponential decay. Use n=1 to measure the harmonics an effect adds. |
burst(f0, levels_db, durs) |
A tone stepping through levels_db, durs seconds each, with continuous phase (quiet, loud, quiet, say) |
harmonics_over_time(y, f0, n=5, periods=4, hop=0.0025) |
Short-time DC and harmonics 1 to n; returns (t, a), where a[i, k] is harmonic k (0 is DC) in the window centred at t[i] |
harmonic_trajectory(f0, levels_db, n=7, **params) |
Steady-state harmonics of a sine at each input level: the cleanup curve |
db(x) |
20 log₁₀ |x|, floored at −240 dB |
save_plot(fig, name) |
Save a matplotlib figure as OUT/<name>.svg |
write_csv(name, header, rows) |
Save the data behind a plot as OUT/<name>.csv |
write_wav(path, y) |
Save a 24-bit mono WAV for listening |
check(cond, msg) |
Record a pass or fail |
finish() |
Print the summary and exit non-zero if any check failed |
plt |
matplotlib.pyplot, with the non-interactive Agg backend already selected |
A minimal test:
"""Silence in must give silence out."""
import numpy as np
from fxtest import SR, check, finish, render
y = render(np.zeros(3 * SR), Gain=100, Level=12)
check(np.max(np.abs(y)) < 1e-6, "silence in gives silence out at full Gain")
finish()
Checks every effect gets¶
tests/run.sh runs the scripts in tests/common/ for every effect after its own (make test runs test_cpu.py separately, see above), reading the controls from build/<name>/controls.json, so they need no per-effect code:
| Test | What it checks |
|---|---|
test_latency.py |
The reported latency is within budget and matches the measured impulse response |
test_zipper.py |
No control clicks when a host jumps it from one end of its range to the other |
test_cpu.py |
CPU per instance, and no subnormal slow-down once the input falls silent |
test_recovery.py |
After full-scale noise, the output decays to silence and stays finite |
Playing feel, CPU and stability explains the limits and what to do when one fails.
What every effect must test¶
| Test | Why |
|---|---|
| Silence in gives silence out, at every extreme setting | Catches denormals, DC offsets, NaNs and self-oscillation |
| Peak ≤ 0 dBFS at the extreme settings, with square waves, noise, sweeps and impulses | Proves the safety limiter holds |
| Frequency response at several settings | Checks the filters and EQ against the circuit notes |
| Harmonic content | Checks the character of the nonlinearity against the reference measurements |
Aliasing, RENDER against RENDER_1X (if the effect oversamples) |
Shows what the oversampling buys |
Effects whose behaviour depends on time or playing level should also test it with the dynamic helpers: a cleanup curve, a quiet–loud–quiet burst and a pluck. Designing for feel covers which measurement shows which mechanism, and tests/midhump/test_dynamics.py is a worked example.
Output formats¶
Results are written in open formats, so they can be reused without reading values off an image:
- Plots: SVG, via
save_plot. - Data: CSV, via
write_csv, with one row per data point in long format. For example,harmonics.csvhas the columnsf0_hz, gain, harmonic, level_dbc. - Listening renders: WAV, via
write_wav. Any audio that gets committed must be FLAC, but test output is never committed.
The plots and CSV files are published on each effect's page on this site, and CI uploads the whole of tests/out/ as the test-output artifact of each run.