The flanger — one comb, three ways of moving it. More...
#include <pulp/signal/character_delay/bbd.hpp>#include <pulp/signal/character_delay/primitives.hpp>#include <pulp/signal/crossfade.hpp>#include <pulp/signal/dc_blocker.hpp>#include <pulp/signal/denormal.hpp>#include <pulp/signal/frequency_shifter_ssb.hpp>#include <pulp/signal/lfo.hpp>#include <pulp/signal/smoothed_value.hpp>#include <pulp/signal/units.hpp>#include <algorithm>#include <array>#include <cmath>#include <cstddef>#include <cstdint>#include <vector>Go to the source code of this file.
Classes | |
| class | pulp::signal::FlangerT< SampleType > |
| The flanger. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
Typedefs | |
| using | pulp::signal::Flanger = FlangerT<float> |
| using | pulp::signal::Flanger64 = FlangerT<double> |
Enumerations | |
| enum class | pulp::signal::FlangerMode : std::uint8_t { pulp::signal::classic , pulp::signal::through_zero , pulp::signal::barberpole } |
| Which of the three sweep topologies the engine runs. More... | |
| enum class | pulp::signal::FlangerPolarity : std::uint8_t { pulp::signal::positive , pulp::signal::negative } |
| The sign of the wet contribution, in the mix and in the feedback tap alike. More... | |
| enum class | pulp::signal::FlangerDelayEngine : std::uint8_t { pulp::signal::clean , pulp::signal::bbd } |
| Which delay engine fills the modulated slot. More... | |
The flanger — one comb, three ways of moving it.
A flanger is a comb filter: a short delay (0.1–10 ms, short enough that the comb's teeth sit at audible, widely-spaced frequencies rather than the dense comb of a chorus's 10–35 ms line) summed with the dry signal, its delay time swept so the whole notch pattern sweeps with it, plus feedback that turns the comb's shallow notches into sharp resonant peaks. The single fact that governs everything here is
(Dattorro 1997 Part 2). Every notch is an artifact of the SAME delay, so they all move in lockstep — that is why a flanger sweep reads as one moving comb, and why a phaser (independent allpass stages, independently placed notches) sounds different even though both are swept notch effects.
polarity is the SIGN OF THE WET CONTRIBUTION, and it is the same sign in every mode and in both the mix and the feedback tap:
Both halves fall out of the same algebra: |1 ± e^{−jωD}|² is 2 ± 2cos(ωD), whose zeros are the two interleaved series above. It also settles which polarity nulls a through-zero crossing, which is not a matter of taste: at the crossing the two paths carry the identical signal, so their SUM is 2x (+6.02 dB, an exact identity) and only their DIFFERENCE is zero. A convention that called the summing case "the one that nulls" would have to mean something different in each mode.
Feedback bound (series laws 1 and 8). There is no saturator in this loop — deliberately: a flanger's resonance is meant to ring, and a waveshaper would blunt exactly the character the control is for — so the entire stability burden sits on the coefficient clamp and must be measured, not assumed. The loop's elements were measured rather than estimated:
kLoopElementGainBound = 1.012 covers all three with headroom, so per-pass loop gain is at most kFbClamp · kLoopElementGainBound = 0.9816 and the steady-state envelope worst_case_gain() is 1/(1 − 0.9816) ≈ 54.5×. That envelope, not the per-pass figure, is what a host needs: a resonant comb at kFbClamp genuinely presents ~30 dB of gain at its peaks.
Aliasing policy (series law 4). N/A for the clean engine: delay lines, LFO-driven fractional interpolation, linear feedback and an equal-power mix are all linear, so there is no nonlinearity generating harmonics for anything to alias. The frequency shifter behind barberpole mode is also linear (it translates the spectrum, it does not add to it) and states its own N/A. The BBD engine is the exception and does not inherit this: it carries a waveshaper and a compander, and its aliasing policy is the one its own module states — internal oversampling by kBbdOversample with the clock-rate bandwidth limit doing the rest.
Determinism (series law 2). The clean and barberpole paths contain no generator at all — a flanger LFO is periodic by definition of the effect, so there is nothing to seed. The BBD engine is the exception again: its clock jitter is stochastic, seeded, and rewound by reset(), so a render from reset() is bit-identical on every engine.
RT contract: prepare(sample_rate) sizes the delay buffers and MAY allocate. set_*, process(), process_stereo() and reset() never allocate, never lock, never perform I/O. State is POD; a zero-initialised instance is a valid silent one. The feedback return is DC-blocked and then denormal-snapped, house convention for any recirculating state that decays toward zero without reaching it.
References: J. Dattorro, "Effect Design, Part 2: Delay-Line Modulation and Chorus", JAES 45(10), pp. 764–788, 1997 (the comb transfer function, the Δf = 1/D relation, and flanging/chorus/doubling as one delay-modulation family). T. I. Laakso, V. Välimäki, M. Karjalainen & U. K. Laine, "Splitting the Unit Delay", IEEE Signal Processing Magazine 13(1), pp. 30–60, 1996 (the fractional-delay interpolator, composed from chardelay::FractionalDelayLine, not reimplemented). F. Esqueda, V. Välimäki & J. Parker, "Barberpole Phasing and Flanging Illusions", Proc. DAFx-15, Trondheim, 2015 (the frequency-shift route to a non-reversing sweep). The positive/negative flanging vocabulary is documented studio practice with no peer-reviewed source; the behaviour it names is derived here from the comb algebra above rather than borrowed.