Fundsp Reverb (Simple variant)
Stereo plate reverb wired through a
fundsp audio graph. Same
topology / params / signal flow as
fundsp-reverb-worker, but the graph
rebuild happens inline on the audio thread instead of on a
background worker.
Source: examples/truce-example-fundsp-reverb-simple/.
The fundsp integration guide walks through both variants in depth, explaining the rebuild trigger, why some params need a rebuild, and what the worker pattern buys you.
in (L,R) ──► high-pass (low cut) ──► low-pass (high cut) ──► reverb_stereo ──┐
│
in (L,R) ─────────────────────────────────────────────────────────────────► dry ┤──► out
│
mix ──────────────┘
| Param | Range |
|---|---|
| Low Cut | 20 Hz → 2 kHz (log) |
| High Cut | 500 Hz → 18 kHz (log) |
| Time | 0.1 s → 20 s (log) |
| Mix | 0 → 1 |
#Integration patterns
- Graph rebuilt inline in
process(). When Time crosses the hysteresis threshold,process()callsrebuild_graphdirectly.Box::new(...)andgraph.allocate()run on the audio thread. The worker variant moves both off-thread; everything else about the graph wiring is identical between the two crates. - Hysteresis on Time changes.
reverb_stereobakes RT60 at construction, so each crossing means a full rebuild. The 5% threshold keeps tiny drifts (smoother ramps, automation jitter) from triggering rebuilds. - Read the raw target, not the smoothed value. A smoothed
time.read()would crawl across the threshold for ~200 ms on each knob move and rebuild every block until it settled — audible as an unstable tail. - Params reach the graph through
fundsp::Sharedatomics.var(&shared)reads them per sample; the closure insidefor_each_framewrites the smoothed truce-side value into the cell on the same tick (sample-accurate automation). Box<dyn AudioUnit>for the field type. The concreteAn<…>is hundreds of chars of nested generics; the vtable cost is one indirection per block.AudioBuffer::for_each_frame::<2, _>transposes truce's per-channel layout into stack-allocated frames so fundsp'stick(in, out)callback can be called directly. No scratch field.
#Gotchas
- Filter input order is positional and unchecked.
highpass()/lowpass()take(signal, cutoff, Q). Every connection isf32, so(cutoff | Q | signal) >> highpass()compiles fine and silently feeds the filter cutoff in as audio — the resulting filter blows up the reverb FDN to peak ~7000 within a second. Test against constant input +assert_peak_below. - Type-level channels.
dry * mixfails to compile whendryis stereo andmixis a 1-channelSharedread; broadcast the mix to stereo manually withvar(&mix) | var(&mix). fundsp's payoff (graph composition with>>/|/&) costs this kind of explicit plumbing.
#Build
cargo build -p truce-example-fundsp-reverb-simple
cargo test -p truce-example-fundsp-reverb-simple --release
cargo truce install -p truce-example-fundsp-reverb-simple
cargo truce run -p truce-example-fundsp-reverb-simple