Offline

Hilbert

exact
time-domainofflinesingle-carrierexact

An envelope detector traces the loudness contour of a waveform — the slow outline riding over the fast carrier inside it. Every graph on this page is drawn by the method's real algorithm, and the sliders at the top drive all of them at once.

The whole method, live

Hilbert
level 0.821280 samples
Hilbert
Smoothing (0 = exact)0 samp (0.0 ms)

Score card

Causality
offline
Signal model
single-carrier
Reads
exact
Latency
none (offline)
Cost
FFT
Domain
time
Reads (measured on a steady sine)1.00×

Tracking error vs the true envelope, by challenge axis — longer bar is a tighter fit. Computed live across the oracle generators.

Temporal
2%
Robust
30%
Spectral
2%
Boundary
10%

How it works

The mathematically exact envelope — no time constant, no ripple. Build the analytic signal (the original plus j times its Hilbert transform) and take its magnitude √(x² + H{x}²). There is no attack/release to tune and nothing to smooth: for a clean tone it returns the amplitude exactly, at every sample.

The catch is that the ideal transform is non-causal — computed here over the whole signal by FFT. A real-time FIR approximation exists but adds latency. Optional smoothing only cleans up wobble on broadband material; on a clean carrier the raw magnitude is already smooth.

Key terms

Analytic signal
The original signal plus j times its Hilbert transform — a complex signal whose magnitude is the instantaneous amplitude at every sample.
Hilbert transform
A 90° phase shift of every frequency component, written H{x}. Pairing it with the original is what makes the analytic signal.
Instantaneous amplitude
The magnitude √(x² + H{x}²) — the exact envelope, defined at every sample with no smoothing or time constant.

Building the envelope, step by step

Each step adds one idea and shows a graph with only that principle applied — drawn by the real algorithm on the page's own input, working up to the finished curve.

  1. Step 1The carrier

    Start with the raw waveform — a fast carrier whose height swells and fades. The envelope is the slow outline we want, not the fast wiggle inside it.

  2. Step 2The analytic magnitude

    Pair the signal with a 90°-phase-shifted copy of itself — its Hilbert transform — to form the analytic signal. Its magnitude is the same above and below zero, so it traces the carrier's amplitude from both sides at once, with no rectifier ripple to smooth away.

  3. Step 3The exact envelope

    Keep the upper magnitude and you have the instantaneous amplitude directly — a smooth line that sits exactly on the carrier's peaks at every sample, with no time constant chosen and nothing averaged away.

The code

Six readable forms of the exact algorithm that draws the curves above — C, JS and Python ports, an optimized C, a fixed-coefficient version, and a user-controlled one whose parameters match the sliders.

#include <math.h>

/* Shared transform (see math.ts fft): in-place radix-2 Cooley-Tukey on the
   complex buffer re[]/im[] of length m (a power of two). inv != 0 runs the
   inverse with 1/m scaling. Not re-derived here — the Hilbert steps are the point. */
void fft(double *re, double *im, int inv);

/* Hilbert envelope: |analytic signal| via FFT. n need not be a power of two;
   we pad up to m. The caller provides scratch re[]/im[] of length m. */
void hilbert_envelope(const double *x, double *env, int n) {
    int m = 1;
    while (m < n) m <<= 1;
    double *re = (double *)calloc(m, sizeof(double));
    double *im = (double *)calloc(m, sizeof(double));

    for (int i = 0; i < n; i++) re[i] = x[i];   /* copy signal, im[] stays 0 */
    fft(re, im, 0);

    /* Build the analytic spectrum: keep DC (0) and Nyquist (m/2) as-is,
       double the positive frequencies, zero the negative ones. */
    int half = m / 2;
    for (int i = 1; i < half; i++) { re[i] *= 2.0; im[i] *= 2.0; }
    for (int i = half + 1; i < m; i++) { re[i] = 0.0; im[i] = 0.0; }

    fft(re, im, 1);                              /* back to time domain */

    for (int i = 0; i < n; i++)                  /* envelope = |analytic| */
        env[i] = hypot(re[i], im[i]);

    free(re);
    free(im);
}