Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
flanger.hpp File Reference

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

Detailed Description

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

notch spacing Δf = 1/D notches at f(k) = (2k+1)/(2D)

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

Three modes, one engine

  • Classic — one modulated delay, feedback, dry + wet. The jet-plane whoosh. D(t) = center ± depth·LFO(t), dry undelayed, latency 0.
  • Through-zero — the tape-studio original. The DRY path carries a fixed delay the swept wet path can cross: D_wet sweeps 0 .. 2·offset and passes exactly through the dry path's delay twice per LFO cycle. At the crossing the two paths hold the identical signal, and the difference of them is silence — a full-band null no single-delay topology can produce, because a delay line cannot run backwards. The fixed dry delay, not a negative wet delay, is what makes "through zero" representable.
  • Barberpole — a comb whose notches climb (or fall) forever without resetting. The delay is FIXED and the wet path is frequency-SHIFTED by a few Hz instead of being swept (Esqueda, Välimäki & Parker, DAFx-15). Each pass through the feedback loop adds another Δf, so the pattern drifts monotonically and wraps — a Shepard-tone illusion in the frequency domain rather than a sweep that turns around.

The one polarity convention, applied everywhere

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:

positive: y = dry + wet → notches at (2k+1)/(2D); TZ crossing DOUBLES
negative: y = dry − wet → notches at k/D, including DC; TZ crossing NULLS

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.

Where the numbers come from

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:

  • the 4-point Lagrange interpolator's magnitude response is exactly 1.000 at DC and rolls off monotonically from there — swept over all fractional offsets and the whole band it never exceeds unity, which is the maximal-flatness property Laakso et al. 1996 describes;
  • the feedback DcBlocker is the only element that can exceed unity, at 2/(1 + p) = 1.000327 at the shipped corner and 48 kHz;
  • the optional BBD engine peaks at 1.0038 small-signal and is steeply compressive above that (0.85 at −20 dBFS, 0.28 at full scale) — its compander and bandwidth limit make it the least dangerous engine in the loop, not the most.

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.