Real-time
Peak
one-poleAn 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
Score card
- Causality
- real-time
- Signal model
- single-carrier
- Reads
- peak ≈ 1.0×
- Latency
- attack/release
- Cost
- trivial
- Domain
- time
Tracking error vs the true envelope, by challenge axis — longer bar is a tighter fit. Computed live across the oracle generators.
How it works
The default smoother: cheap, causal, and endlessly tunable. Rectify with |x|, then run a one-pole low-pass with separate attack and release coefficients so the curve can rise fast and fall slow — exactly the attack/release shape a compressor or gate wants.
Watch out: it settles toward the average of the rectified signal, not the true peak, and if you set either time shorter than the carrier period it starts tracking individual cycles and ripples.
Key terms
- Rectifier
- The |x| step that folds the negative half of the waveform upward, the way a diode does in an analog detector. Every sample becomes positive so a smoother can settle toward amplitude instead of averaging back to zero.
- One-pole low-pass
- A single-coefficient smoother: y += k · (x − y). Each output is nudged a fraction k of the way toward the new input — a leaky integrator that melts ripple into a smooth line.
- Attack / release
- The two time constants of the smoother: attack sets how fast the curve rises onto a louder section, release how slowly it falls back. Splitting them is what gives the envelope its fast-up, slow-down shape.
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.
- 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.
- Step 2Rectify
Fold the negative half upward with |x|, the way a diode does in an analog detector. Every sample is now positive, so a smoother can settle toward the amplitude instead of averaging back to zero.
- Step 3One-pole smoothing
Low-pass the rectified signal with a one-pole filter — rising on a fast attack, falling on a slow release. The ripple melts into a smooth line that hugs the swell: the envelope.
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>
/* exp(-1/tau): the one-pole coefficient for a time constant of
tau samples. Clamped at 1 so a tiny tau can't blow up. */
static double decay_coeff(double tau) {
return exp(-1.0 / (tau < 1.0 ? 1.0 : tau));
}
/* Peak envelope: rectify, then a one-pole low-pass that rises on the
attack coefficient and falls on the (slower) release coefficient. */
void peak_envelope(const double *x, double *env, int n,
double attack_samples, double release_samples) {
double ca = decay_coeff(attack_samples);
double cr = decay_coeff(release_samples);
double e = 0.0;
for (int i = 0; i < n; i++) {
double r = fabs(x[i]);
double c = (r > e) ? ca : cr; /* faster when rising */
e = c * e + (1.0 - c) * r;
env[i] = e;
}
}
// exp(-1/tau): one-pole coefficient for a tau-sample time constant.
const decayCoeff = (tau) => Math.exp(-1 / Math.max(1, tau));
// Peak envelope: rectify, then one-pole low-pass with split attack/release.
function peakEnvelope(x, attackSamples, releaseSamples) {
const ca = decayCoeff(attackSamples);
const cr = decayCoeff(releaseSamples);
const env = new Float64Array(x.length);
let e = 0;
for (let i = 0; i < x.length; i++) {
const r = Math.abs(x[i]);
const c = r > e ? ca : cr; // faster when rising
e = c * e + (1 - c) * r;
env[i] = e;
}
return env;
}
import math
def decay_coeff(tau):
"""One-pole coefficient exp(-1/tau) for a tau-sample time constant."""
return math.exp(-1.0 / max(1.0, tau))
def peak_envelope(x, attack_samples, release_samples):
"""Rectify, then one-pole low-pass with split attack/release."""
ca = decay_coeff(attack_samples)
cr = decay_coeff(release_samples)
env = [0.0] * len(x)
e = 0.0
for i, xi in enumerate(x):
r = abs(xi)
c = ca if r > e else cr # faster when rising
e = c * e + (1.0 - c) * r
env[i] = e
return env
Three changes, no change in output: the update is rewritten as e += k·(r−e) — one fused multiply-add instead of two multiplies and an add; the (1−c) gains are hoisted out of the loop; and `restrict` tells the compiler x and env don't alias, so it can keep e in a register and vectorize the rectify.
#include <math.h>
void peak_envelope_opt(const double *restrict x, double *restrict env,
int n, double attack_samples, double release_samples) {
const double ka = 1.0 - exp(-1.0 / (attack_samples < 1 ? 1 : attack_samples));
const double kr = 1.0 - exp(-1.0 / (release_samples < 1 ? 1 : release_samples));
double e = 0.0;
for (int i = 0; i < n; i++) {
double r = fabs(x[i]);
double k = (r > e) ? ka : kr;
e += k * (r - e); /* one FMA */
env[i] = e;
}
}
Coefficients hard-coded for attack = 4 samples and release = 32 samples (the page defaults). No tuning knobs — the smallest possible kernel for a known signal.
#include <math.h>
void peak_envelope_fixed(const double *x, double *env, int n) {
const double ca = 0.7788008; /* exp(-1/4) */
const double cr = 0.9692332; /* exp(-1/32) */
double e = 0.0;
for (int i = 0; i < n; i++) {
double r = fabs(x[i]);
double c = (r > e) ? ca : cr;
e = c * e + (1.0 - c) * r;
env[i] = e;
}
}
The two sliders map straight onto the time constants. Larger Attack (1–1000 samp) slows the rise, so the curve lags transients; larger Release (1–4000 samp) slows the fall, so it holds level after a peak. Set either below the carrier period and it starts tracking individual cycles and ripples.
#include <math.h>
static double decay_coeff(double tau) {
return exp(-1.0 / (tau < 1.0 ? 1.0 : tau));
}
void peak_envelope_ctl(const double *x, double *env, int n,
int attack_samples, /* slider: 1 .. 1000 */
int release_samples) { /* slider: 1 .. 4000 */
double ca = decay_coeff(attack_samples);
double cr = decay_coeff(release_samples);
double e = 0.0;
for (int i = 0; i < n; i++) {
double r = fabs(x[i]);
double c = (r > e) ? ca : cr;
e = c * e + (1.0 - c) * r;
env[i] = e;
}
}
