Modulation chorus · flanger · phaser

A single low-frequency oscillator, one swept delay line, and a chain of allpass sections — the three classic ways of putting motion into a sound. Historically grounded theory, correct DSP, and a live plugin whose on-screen code is exactly what you hear.

ChorusFlangerPhaser
Begin ↓

01Movement

Chorus, flanger and phaser are usually sold as three effects. They are better understood as one idea seen from three angles: take a copy of the signal, delay or phase-shift it by an amount that changes slowly over time, and add it back to the original. The interference between the two copies sweeps through the spectrum, and the ear reads that sweep as shimmer, width, or a whooshing jet.

All three are linear time-varying systems. At any frozen instant the effect is an ordinary linear filter — a comb, or an allpass cascade — with a perfectly well-defined frequency response. What makes them alive is that a control signal, the low-frequency oscillator (LFO), is continually nudging the filter's parameters, so that response glides. Freeze the LFO and a flanger becomes a static comb filter; a phaser becomes a fixed set of notches. Let it run and the notches march up and down the spectrum.

The distinctions between the three are matters of degree and mechanism, and they are worth stating precisely at the outset because the rest of the page turns on them:

This page builds each mechanism from its difference equation, then lets you hear all three from one hand-written processing node in the live plugin. The DSP you read in section 8 is, byte for byte, the DSP that renders the audio — the code listing is populated from the same source string that is compiled into the AudioWorklet.

02A short history of putting sound in motion

Modulation effects predate digital signal processing by decades. Every one of them began as a mechanical or electro-mechanical accident that engineers learned to control.

1940s
The Leslie rotary speaker. Donald Leslie builds a cabinet with a rotating horn and drum so that a Hammond organ's sound physically moves around the room. The spinning source produces genuine Doppler pitch shift and amplitude tremolo — the acoustic ancestor of chorus and vibrato.
Late 1960s
Tape flanging / ADT. Engineers at Abbey Road, among them Ken Townsend, feed a signal through two tape machines and drag a finger on the flange of one reel to slow it a fraction. The drifting delay between the two copies sweeps a comb filter — the effect the Beatles nicknamed after the reel flange. The Small Faces' Itchycoo Park (1967) is an early showcase of the swooshing sound.
1968
The Uni-Vibe. Intended as a Leslie emulator, Fumio Mieda's photocell phase-shifter instead produces a watery four-stage phaser. Hendrix and Gilmour make it iconic; it is, in effect, the first mass-market phaser pedal.
1970s
Analogue BBD choruses and phasers. The bucket-brigade delay (BBD) chip makes short analogue delays cheap. The MXR Phase 90 (1974) and Electro-Harmonix Small Stone bring the phaser to every pedalboard; Boss's CE-1 (1976) delivers studio-grade chorus in a box.
1976–1982
Chorus enters the machine. Roland's Jazz Chorus amp and, above all, the Juno-6/Juno-60 (1982) bake a BBD stereo chorus into the synth itself; its noisy, wide "Juno chorus" becomes a genre. Roland's Dimension D (SDD-320) offers a pristine studio variant.
1990s→
The DAW plug-in era. Digital delay lines with fractional interpolation replace tape and BBD. The physics is unchanged — a modulated delay, an allpass chain — but now the delay is a ring buffer and the LFO is a variable in a processing loop, exactly as below.

03The shared engine: one LFO

Every modulation effect on this page is driven by the same low-frequency oscillator. It is not in the audio path — you never hear it directly. It is a control signal, running at a rate typically between $0.05$ and $8\ \mathrm{Hz}$, that sets a filter parameter each sample.

A sinusoidal LFO of rate $f_r$ produces

$$m(t)=\sin(2\pi f_r t),\qquad m[n]=\sin\!\big(2\pi f_r n/f_s\big)=\sin(\phi_n),\quad \phi_{n}=\phi_{n-1}+\tfrac{2\pi f_r}{f_s}.$$

In code the LFO is a single phase accumulator: add a small increment $\Delta\phi = 2\pi f_r/f_s$ each sample and wrap at $2\pi$. From that one running phase we derive the modulated delay time (flanger, chorus) or the allpass break frequency (phaser). A depth control scales the excursion, and a stereo spread control offsets the phase of the right channel by up to $\pi$ so the two channels sweep out of step — the single most important trick for making the effect sound wide.

The generic modulated parameter is an offset plus a scaled LFO:

$$p[n] = p_0 + d\cdot m[n],$$

where $p_0$ is a centre value and $d$ the depth. For a flanger $p$ is a delay in samples; for a phaser it is (via a logarithmic map) a break frequency in hertz. The waveform need not be a sine — triangle gives a linear sweep, and a randomised/smoothed shape gives a slow, unpredictable drift — but the sine is the canonical choice and the one used below.

The shared LFO. Teal = left channel, magenta = right channel offset by the stereo-spread phase. A moving playhead marks the current sample.

04Flanger — the swept comb

Add a signal to a short delayed copy of itself and you build a comb filter. Sweep the delay and the comb slides. That is the whole of a flanger.

With a delay of $D$ samples and a mix gain $g$, the feed-forward difference equation is

$$y[n] = x[n] + g\,x[n-D(n)],$$

where $D(n)$ is the LFO-modulated delay. Take the $z$-transform at a frozen $D$ and the transfer function is

$$H(z) = 1 + g\,z^{-D},\qquad |H(e^{j\omega})|^2 = 1 + g^2 + 2g\cos(\omega D).$$

The magnitude has nulls wherever $\cos(\omega D) = -1$, i.e. $\omega D = (2k+1)\pi$. Converting $\omega = 2\pi f/f_s$ and $D = f_s \tau$ (with $\tau$ the delay in seconds) gives notch frequencies

$$f_k = \frac{2k+1}{2\tau} = \frac{k+\tfrac12}{\tau},\qquad k = 0,1,2,\dots$$

These are the flanger's signature: notches at odd multiples of $1/(2\tau)$, harmonically spaced and stacked all the way up the spectrum like the teeth of a comb. A $1\ \mathrm{ms}$ delay puts the first notch at $500\ \mathrm{Hz}$ and repeats every $1\ \mathrm{kHz}$. As the LFO shortens and lengthens $\tau$, every notch slides together — the entire comb breathes.

Feedback: from comb to resonant comb

Routing the delayed output back into the delay input turns the feed-forward comb into a feedback (IIR) comb:

$$y[n] = x[n] + g\,w[n-D],\qquad w[n] = x[n] + f\,w[n-D],$$

whose frozen transfer function has poles as well as zeros,

$$H(z) = 1 + \frac{g\,z^{-D}}{1 - f\,z^{-D}}.$$

The feedback coefficient $f$ (kept $|f|<1$ for stability) sharpens the notches into resonant peaks and troughs. Push it towards unity and the sweeping comb rings — this is the aggressive metallic "jet plane" flange. Negative feedback shifts the notch pattern to even harmonics and gives a thinner, hollow character.

Frozen flanger magnitude response (log frequency). Notches sit at (k+½)/τ; feedback deepens and sharpens them. Drag the delay to slide the whole comb.

05Chorus — a moving delay is a pitch shift

Lengthen the delay past about ten milliseconds and the comb notches fall so close together that the ear no longer resolves them. What you hear instead is the consequence of the delay changing: a moving delay line is a pitch shifter.

Reading a delay line at a time-varying delay $D(t)$ is a resampling operation. If the instantaneous output samples the input at $t-\tau(t)$, then the output's instantaneous frequency is scaled by the rate of change of that read pointer. Differentiating the phase gives the fractional pitch shift

$$\frac{\Delta f}{f} = -\frac{d\tau}{dt}.$$

This is precisely the Doppler effect: when the delay is lengthening ($d\tau/dt>0$) the virtual source recedes and the pitch drops; when the delay shortens the pitch rises. A sinusoidal LFO of rate $f_r$ and depth $\tau_d$ gives $\tau(t)=\tau_0+\tau_d\sin(2\pi f_r t)$, so

$$\frac{\Delta f}{f} = -2\pi f_r\,\tau_d\cos(2\pi f_r t),$$

a gentle vibrato whose peak deviation is $2\pi f_r \tau_d$. With $f_r = 0.5\ \mathrm{Hz}$ and $\tau_d = 3\ \mathrm{ms}$ the deviation is only about $\pm0.9\%$ — a few cents — which is exactly the subtle detune the ear reads as "another player", not "out of tune".

Why several voices widen it

A single detuned copy sounds like a doubling; an ensemble sounds like a section. The trick is decorrelation: run several delay lines, each with its own LFO phase (and ideally a slightly different rate and centre delay), and sum them. Because their pitch wobbles are out of step, the copies drift in and out of unison independently, and the summed comb pattern is smeared rather than periodic. Panning the voices, or giving the left and right channels opposed LFO phases via the stereo-spread control, turns that decorrelation into stereo width. This is why the Juno chorus — two BBD lines with opposed triangle LFOs — sounds so much wider than a single-line pedal.

Chorus normally uses little or no feedback: the point is thickening, not resonance. Adding feedback pulls the character back towards flanging, which is why many units are really one delay line with a mode switch — the same architecture, different delay range, depth and feedback. That is exactly how the plugin below is built.

Per-voice delay-time modulation traces. Each voice sweeps on its own LFO phase; their independence is what produces width. The instantaneous pitch deviation is proportional to each trace's slope.

06Phaser — a cascade of allpass sections

A flanger delays every frequency by the same amount of time. A phaser instead phase-shifts different frequencies by different amounts, using allpass filters, so its notches are not forced into a harmonic comb.

A first-order allpass has unity gain at every frequency but a frequency-dependent phase. In the sign convention used in the code below,

$$H_{\mathrm{ap}}(z) = \frac{-a + z^{-1}}{1 - a\,z^{-1}},\qquad |H_{\mathrm{ap}}(e^{j\omega})| = 1\ \ \forall\,\omega.$$

Its phase runs from $0$ at DC to $-\pi$ at Nyquist, passing through $-\pi/2$ at a break frequency $f_b$ set by the coefficient. Mapping a desired break frequency to the coefficient via the bilinear tangent,

$$a = \frac{\tan(\pi f_b/f_s) - 1}{\tan(\pi f_b/f_s) + 1},\qquad -1 < a < 1.$$

Cascade $N$ such sections and their phases add: the total phase reaches $-N\pi/2$ at $f_b$ and continues to $-N\pi$ at Nyquist. Now sum this allpass output with the dry signal:

$$y[n] = x[n] + m\,\big(H_{\mathrm{ap}}^{N}\!\ast x\big)[n].$$

Wherever the cascade's phase passes through an odd multiple of $\pi$, the delayed copy is in antiphase with the dry signal and a notch appears. A cascade of $N$ first-order sections produces about $\lfloor N/2\rfloor$ notches in the audio band. Sweeping the coefficient $a$ with the LFO drags every break frequency, and the notches glide.

The crucial difference from the flanger: the allpass phase curve is not linear in $\omega$, so the frequencies where the phase hits $\pi, 3\pi, 5\pi,\dots$ are not evenly spaced. Phaser notches are non-uniform, their count fixed by the number of stages rather than by a delay time. A four-stage MXR Phase 90 gives two notches; a Small Stone or a "ten-stage" digital phaser gives more, deeper sweeps. Feedback around the cascade sharpens the notches, just as it does for the flanger.

Frozen phaser magnitude response. Notches occur where the allpass-cascade phase crosses odd multiples of π — note the non-uniform spacing, unlike the flanger comb. More stages, more notches.

07Fractional delay and the zipper problem

The modulated delay $D(n)$ is almost never a whole number of samples. Reading the delay line at an integer index and snapping $D(n)$ to the nearest sample produces audible steps — "zipper noise" — every time the pointer jumps. Smooth modulation demands a fractional read.

The simplest interpolator is linear. To read the delay line at fractional position $D = i + \delta$ (integer part $i$, fraction $0\le\delta<1$):

$$y[n] = (1-\delta)\,w[n-i] + \delta\,w[n-i-1].$$

Linear interpolation is cheap and unconditionally stable, and it is what the plugin below uses. Its cost is a mild, delay-dependent low-pass tilt — the interpolator attenuates high frequencies most when $\delta=0.5$ — which for chorus and flanger is inaudible against the effect itself.

Where the very top octave must be preserved, two better options exist:

Whatever the kernel, the deeper requirement is that the read pointer move continuously. As long as $D(n)$ is a smooth function of the LFO and the interpolation is at least linear, the delay glides without zipper artefacts. Julius Smith's treatment of time-varying delay lines in Physical Audio Signal Processing is the definitive reference for choosing between these kernels.

In the plugin, both chorus and flanger read their ring buffers with the linear interpolator shown above; the phaser needs no delay line at all, since its allpass state is inherently fractional in phase. This is why the phaser is immune to zipper noise even under fast modulation.

08Live plugin — one node, three effects

Everything above, running in your browser. A single AudioWorklet holds one shared LFO, one pair of interpolated delay lines and one allpass chain; the mode switch chooses which path is active. The code listing beneath the plugin is filled in from the very same source string that is compiled into the worklet — what you read is what you hear.

Modulation — CH / FL / PH DSP & MUSIC · TELETYPE SERIES
Rate
0.40 Hz
Depth
50%
Feedback
20%
Mix
50%
Spread
40%
Stages
4
Voices
3
Out
drop an audio file anywhere on the panel

The exact DSP running above — a hand-written modulated-delay + allpass engine. Chorus/flanger read interpolated delay lines; the phaser runs a first-order allpass cascade. All three share one LFO phase.

loading…

Reading the display

The main graph draws the effect's instantaneous magnitude response, recomputed every animation frame from the live LFO phase, so you watch the notches sweep in real time — a harmonic comb in flanger mode, a smeared comb in chorus, and the non-uniform allpass notches in phaser. Below the plugin, three further canvases expose the internals.

LFO waveform with moving playhead (left channel teal, right channel magenta by the spread offset).
Modulation trace — swept delay time (chorus/flanger) or break frequency (phaser), scrolling.
Stereo vectorscope (L vs R). Width opens as the spread control decorrelates the channels.
Safety: the master path runs through a light limiter and the output is metered; high feedback on the flanger is deliberately allowed to ring, but is clamped short of runaway. If you hear nothing, click Play once more — browsers require a user gesture before audio can start.

09The same effect in JUCE

In a real plugin you would lean on juce::dsp for the chorus and flanger and hand-roll the allpass cascade for the phaser, all driven by a single juce::dsp::Oscillator LFO and exposed through an AudioProcessorValueTreeState.

The chorus and flanger are the same object — juce::dsp::Chorus is a modulated delay with feedback, so a "flanger" is simply that class configured with a short centre delay and higher feedback. The phaser is a cascade of first-order allpass sections; juce::dsp::FirstOrderTPTFilter in allpass mode, or a bank of juce::dsp::IIR::Filter with allpass coefficients, does the job.

// ---- ModulationProcessor.h -------------------------------------------
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_dsp/juce_dsp.h>

class ModulationProcessor : public juce::AudioProcessor
{
public:
    ModulationProcessor();
    void prepareToPlay (double sampleRate, int samplesPerBlock) override;
    void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
    // ... boilerplate elided ...

    juce::AudioProcessorValueTreeState apvts;
    static juce::AudioProcessorValueTreeState::ParameterLayout layout();

private:
    enum Mode { Chorus = 0, Flanger, Phaser };

    juce::dsp::Chorus<float>      chorus;   // used for CHORUS and FLANGER
    juce::dsp::Oscillator<float>  lfo;      // shared LFO for the phaser

    // hand-rolled first-order allpass cascade for the phaser
    static constexpr int kMaxStages = 8;
    std::array<juce::dsp::IIR::Filter<float>, kMaxStages> apL, apR;
    std::array<float, 2> fbState { 0.0f, 0.0f };
    double fs = 44100.0;
};

// ---- ModulationProcessor.cpp -----------------------------------------
juce::AudioProcessorValueTreeState::ParameterLayout
ModulationProcessor::layout()
{
    using P = juce::AudioParameterFloat;
    using C = juce::AudioParameterChoice;
    juce::AudioProcessorValueTreeState::ParameterLayout l;
    l.add (std::make_unique<C>("mode", "Mode",
              juce::StringArray{ "Chorus","Flanger","Phaser" }, 0));
    l.add (std::make_unique<P>("rate",  "Rate",
              juce::NormalisableRange<float>(0.05f, 8.0f, 0.0f, 0.3f), 0.4f));
    l.add (std::make_unique<P>("depth", "Depth", 0.0f, 1.0f, 0.5f));
    l.add (std::make_unique<P>("fb",    "Feedback", -0.95f, 0.95f, 0.2f));
    l.add (std::make_unique<P>("mix",   "Mix",   0.0f, 1.0f, 0.5f));
    l.add (std::make_unique<juce::AudioParameterInt>("stages","Stages",2,8,4));
    return l;
}

void ModulationProcessor::prepareToPlay (double sampleRate, int block)
{
    fs = sampleRate;
    juce::dsp::ProcessSpec spec { sampleRate,
                                  (juce::uint32) block,
                                  (juce::uint32) getTotalNumOutputChannels() };
    chorus.prepare (spec);
    lfo.initialise ([] (float x) { return std::sin (x); }, 128);
    lfo.prepare (spec);
    for (auto& f : apL) f.prepare (spec);
    for (auto& f : apR) f.prepare (spec);
}

// first-order allpass coefficient for a given break frequency
static inline float allpassCoeff (float fb, double fs)
{
    const float t = std::tan (juce::MathConstants<float>::pi * fb / (float) fs);
    return (t - 1.0f) / (t + 1.0f);              // matches H(z) = (-a + z^-1)/(1 - a z^-1)
}

void ModulationProcessor::processBlock (juce::AudioBuffer<float>& buffer,
                                        juce::MidiBuffer&)
{
    const int mode   = (int)  apvts.getRawParameterValue ("mode")  ->load();
    const float rate = apvts.getRawParameterValue ("rate")  ->load();
    const float depth= apvts.getRawParameterValue ("depth") ->load();
    const float fb   = apvts.getRawParameterValue ("fb")    ->load();
    const float mix  = apvts.getRawParameterValue ("mix")   ->load();
    const int stages = (int)  apvts.getRawParameterValue ("stages")->load();

    if (mode == Chorus || mode == Flanger)
    {
        // FLANGER = short centre delay + more feedback; CHORUS = long delay, wide depth
        const float centreMs = (mode == Flanger) ? 2.0f : 20.0f;
        chorus.setRate (rate);
        chorus.setDepth (depth);
        chorus.setCentreDelay (centreMs);
        chorus.setFeedback (mode == Flanger ? fb : fb * 0.3f);
        chorus.setMix (mix);
        juce::dsp::AudioBlock<float> block (buffer);
        chorus.process (juce::dsp::ProcessContextReplacing<float> (block));
        return;
    }

    // ---- PHASER: swept allpass cascade summed with dry ----
    lfo.setFrequency (rate);
    auto* L = buffer.getWritePointer (0);
    auto* R = buffer.getNumChannels() > 1 ? buffer.getWritePointer (1) : L;

    for (int n = 0; n < buffer.getNumSamples(); ++n)
    {
        const float m  = 0.5f + 0.5f * lfo.processSample (0.0f);   // 0..1
        const float fbHz = 200.0f * std::pow (11.0f, m * (0.4f + 0.6f * depth));
        const float a  = allpassCoeff (fbHz, fs);
        const auto  co = juce::dsp::IIR::Coefficients<float>::Ptr (
                             new juce::dsp::IIR::Coefficients<float> (-a, 1.0f, 1.0f, -a));

        auto stage = [&] (float x, auto& chain, float& fbSt) -> float
        {
            float v = x + fb * fbSt;
            for (int s = 0; s < stages; ++s) { chain[s].coefficients = co; v = chain[s].processSample (v); }
            fbSt = v;
            return x + mix * v;
        };
        L[n] = stage (L[n], apL, fbState[0]);
        R[n] = stage (R[n], apR, fbState[1]);
    }
}

Idiomatic JUCE: APVTS-backed parameters, juce::dsp::Chorus reused for chorus and flanger, a hand-rolled IIR allpass cascade for the phaser, all sharing one juce::dsp::Oscillator. The allpassCoeff helper is the same tangent map used in the JavaScript, so the two implementations agree sample-for-sample in the frozen limit.

10Reference & further reading

The three effects compared

PropertyFlangerChorusPhaser
MechanismShort modulated delay + dryLong modulated delay(s) + drySwept allpass cascade + dry
Delay range~0.1–10 ms~10–40 msnone (allpass phase)
NotchesHarmonic comb, evenly spaced $1/\tau$Comb too dense to resolveNon-uniform, count $=\lfloor N/2\rfloor$
FeedbackHigh — resonant "jet"Low / noneOptional — sharpens notches
Perceived asSweeping metallic whooshThickening, width, ensembleHollow vowel-like sweep
Needs interpolationYes (fractional delay)Yes (fractional delay)No
Classic unitA/DR Flanger, MXRRoland Juno / Dimension DMXR Phase 90, Small Stone

References

  1. Zölzer, U. (ed.), DAFX: Digital Audio Effects, 2nd ed., Wiley, 2011 — chapters on modulators and time-varying delay-line effects (chorus, flanger, phaser).
  2. Dattorro, J., "Effect Design, Part 2: Delay-Line Modulation and Chorus," Journal of the Audio Engineering Society, 45(10), 1997.
  3. Smith, J. O., Physical Audio Signal Processing, online edition, CCRMA, Stanford — time-varying delay lines and fractional-delay interpolation.
  4. Smith, J. O., Introduction to Digital Filters, with Audio Applications, CCRMA — first-order allpass sections and phaser design.
  5. Hartmann, W. M., "Flanging and phasers," in Signals, Sound, and Sensation, Springer, 1997.
  6. Roland Corporation, Juno-60 Service Notes — BBD chorus circuit and dual-LFO configuration.
  7. Pirkle, W., Designing Audio Effect Plugins in C++, 2nd ed., Routledge, 2019 — practical chorus/flanger/phaser implementations.