The chorus family — one engine, four documented voicings selected as data. More...
#include <pulp/signal/character_delay/bbd.hpp>#include <pulp/signal/delay_line.hpp>#include <pulp/signal/interpolator.hpp>#include <pulp/signal/lfo.hpp>#include <pulp/signal/tpt_filter.hpp>#include <pulp/signal/units.hpp>#include <algorithm>#include <array>#include <cmath>#include <cstddef>#include <cstdint>Go to the source code of this file.
Classes | |
| class | pulp::signal::ChorusEnsembleT< SampleType > |
| One chorus engine, four voicings. Stereo in place. More... | |
| struct | pulp::signal::ChorusEnsembleT< SampleType >::Calibration |
| One voicing's shipped calibration. More... | |
| struct | pulp::signal::ChorusEnsembleT< SampleType >::JunoModeSpec |
| The Juno's per-mode calibration. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
Typedefs | |
| using | pulp::signal::ChorusEnsemble = ChorusEnsembleT<float> |
| using | pulp::signal::ChorusEnsemble64 = ChorusEnsembleT<double> |
The chorus family — one engine, four documented voicings selected as data.
A chorus is a modulated short delay line mixed with the dry signal. What separates a Boss CE-2 from a Juno ensemble from a Roland Dimension D from a three-voice TriChorus is not the delay and not the interpolator: it is how many taps there are, what phase relationship their modulators hold, and how the taps are summed back into L and R. So the frame here — a fractional delay read, an LfoT per voice, a dry/wet blend — is shared, and each voicing is a row in a calibration table plus a branch in the mix matrix. Nothing about a voicing's identity lives in a different code path through the delay or the interpolation core, which is what makes the four auditable against each other.
pulp::signal::ChorusT (chorus.hpp) is an earlier, thinner prototype: a single sine LFO, a quadrature-offset stereo pair, depth expressed as a fraction of the centre delay, and linear interpolation. It is not a subset of this class — its depth law and its 90°-offset stereo image belong to no documented hardware voicing — so this module ships alongside it rather than extending it in place. ChorusT is public API (signal.hpp exports it, test/test_signal.cpp covers it); removing it is a coordinated public-API change, not something to fold into this module's landing. New callers should use ChorusEnsembleT; ChorusT is superseded and should be retired in a follow-up that also updates its exporter and its test.
Published, cited at the constant: the Juno's three-mode delay ranges and LFO rates (pendragon-andyh's measured Juno-60 circuit analysis) and the CE-2's practical 5–40 ms delay zone (ElectroSmash's open circuit analysis of the MN3007/MN3101 pair). Everything else is a design parameter carrying a default and a range: the CE-2's exact centre/depth/rate inside its cited zone (Boss never published pot calibration curves), and the whole of the Dimension D and TriChorus calibrations (no source documents either family at that precision — an honest gap, not an omission).
The audio path is linear when bbd_color is off, so there is nothing to oversample. Three things could be mistaken for a nonlinearity:
latency_samples() is 0 for every voicing and every mode. The Lagrange stencil spans kInterpOrder + 1 samples around the fractional read position, so it stays strictly behind the write pointer as long as the minimum instantaneous delay stays kGuardSamples clear of it. Every shipped calibration clears that guard by a factor of 19 or more — the tightest is the Juno's 79.7 samples of minimum delay against a 4-sample guard at 48 kHz. Enabling bbd_color only adds group delay (measured: 3 samples, from the composed stage's band-limiting pair and its clocked write/read alignment), never lookahead.
RT contract: prepare() allocates the two delay lines and is the only function in this class that may. set_*, process, and reset allocate nothing, take no locks, and perform no I/O. set_voicing/set_juno_mode recompute filter and BBD coefficients, so they are control-thread calls; they are allocation-free and safe to call from the audio thread if a host insists. Every buffer is sized in prepare() for kMaxDelayMs, which covers the widest retune the calibration tables' declared ranges allow, so no set_* can outgrow the storage.