Offline

Peak interp

fit-first
time-domainofflinesingle-carrierpeak ≈ 1.0×

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

Peak interp
level 0.811280 samples
Hilbert refPeak interp
Min peak distance8 samp (0.2 ms)
Interpolation

Score card

Causality
offline
Signal model
single-carrier
Reads
peak ≈ 1.0×
Latency
none (offline)
Cost
moderate
Domain
time
Reads (measured on a steady sine)0.99×

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

Temporal
36%
Robust
24%
Spectral
3%
Boundary
3%

How it works

The tightest raw fit — a curve drawn straight through the peaks. Detect the local maxima of |x|, then interpolate between them. Linear connects the dots; PCHIP (monotone cubic) curves smoothly without overshooting; cubic spline is smoothest but rings — it overshoots peaks and can dip below zero.

This is the detector to reach for when fidelity to the waveform's outline matters most, e.g. for EMD or display. Its weakness is the peak picking: too large a minimum distance and it skips real peaks.

Key terms

Local maximum / peak picking
A sample larger than its neighbours in |x| — a tip of the rectified waveform. These are the knots the envelope is drawn through; picking them well is the whole job.
Minimum peak distance
The control that suppresses any peak sitting closer than a set spacing to a taller one, so a single swell yields one knot instead of a cluster. Set it too large and it starts skipping real peaks, flattening the outline.
Interpolation kind
How the curve joins the knots. Linear connects the dots with straight segments; PCHIP (monotone cubic) curves smoothly without overshooting; a cubic spline is smoothest of all but rings.
Overshoot / ringing
A spline curving past the peaks between knots — bulging above the tips and even dipping below zero where the signal never went. The price of the smoothest fit.

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, the shape that touches the tips of the waveform.

  2. Step 2Rectify

    Fold the negative half upward with |x|. Every sample is now positive, so the upper edge of the waveform — the peaks we are about to pick — is one consistent set of local maxima rather than two.

  3. Step 3Pick peaks and interpolate

    Find every local maximum of the rectified signal (suppressing peaks closer than the minimum distance, keeping the taller), then draw a smooth curve through those knots. Here PCHIP connects them — the fitted outline rides exactly on the peak tips.

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>

/* Local maxima of the rectified signal, with min-distance suppression: when two
   peaks sit closer than min_dist, keep only the taller. Returns the count and
   fills idx[]. */
static int find_peaks(const double *r, int n, int min_dist, int *idx) {
    if (min_dist < 1) min_dist = 1;
    int k = 0;
    for (int i = 1; i < n - 1; i++) {
        if (r[i] >= r[i - 1] && r[i] > r[i + 1]) {       /* a local max */
            if (k && i - idx[k - 1] < min_dist) {
                if (r[i] > r[idx[k - 1]]) idx[k - 1] = i; /* taller wins */
            } else {
                idx[k++] = i;
            }
        }
    }
    return k;
}

/* Peak interpolation (linear): rectify, find peaks, then draw straight segments
   through the knots [0, ...peaks, n-1]. The ends hold flat (t clamped to [0,1]).
   PCHIP and natural-cubic swap in here, replacing only this segment formula. */
void peak_interp_linear(const double *x, double *env, int n, int min_dist) {
    double *r = (double *)malloc(n * sizeof(double));
    int *pk = (int *)malloc(n * sizeof(int));
    for (int i = 0; i < n; i++) r[i] = fabs(x[i]);

    int np = find_peaks(r, n, min_dist, pk);

    /* knots: 0, the peaks, n-1 (skip any that coincide) */
    int *xs = (int *)malloc((np + 2) * sizeof(int));
    int m = 0;
    xs[m++] = 0;
    for (int i = 0; i < np; i++) if (pk[i] != xs[m - 1]) xs[m++] = pk[i];
    if (n - 1 != xs[m - 1]) xs[m++] = n - 1;

    /* sweep segment k forward as i advances; linear-interpolate within it */
    int k = 0;
    for (int i = 0; i < n; i++) {
        while (k < m - 2 && xs[k + 1] < i) k++;
        int x0 = xs[k], x1 = xs[k + 1];
        double t = (x1 == x0) ? 0.0 : (double)(i - x0) / (x1 - x0);
        if (t < 0) t = 0; else if (t > 1) t = 1;
        env[i] = r[x0] + (r[x1] - r[x0]) * t;
    }

    free(r); free(pk); free(xs);
}