Real-time
RMS
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
- RMS ≈ 0.707×
- Latency
- release-dominated
- 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
Measures power, not crests — the loudness detector. Square the signal, smooth the energy with a one-pole low-pass, then take the square root. It's the contour that tracks perceived level rather than transient peaks.
The squaring pre-averages the waveform, so the curve is inherently smooth and slow to react, and it reads ≈0.707·A for a sine of amplitude A. Reach for it when you care about sustained energy or loudness — a loudness meter or a sidechain — rather than the sharp crests a limiter has to catch.
Key terms
- Mean-square / power
- Squaring every sample gives instantaneous power: x². Always positive, it weights loud excursions far more than quiet ones, so the smoothed result follows energy rather than raw amplitude.
- RMS
- Root-mean-square — average the power, then take its square root. For a sine of amplitude A the RMS level is 0.707·A, the value that tracks perceived loudness.
- 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.
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 2Square the signal
Instead of |x|, square every sample. Squaring is also always positive, but it weights loud excursions more — the wave becomes instantaneous power.
- Step 3Smooth, then root
Average that energy with a one-pole low-pass and take the square root. The result reads ≈0.707·A for a sine of amplitude A — the RMS envelope, smooth and slow to react.
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));
}
/* RMS envelope: square, smooth the energy with a one-pole low-pass
(faster attack coefficient, slower release), then take the root. */
void rms_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 = x[i] * x[i]; /* instantaneous power */
double c = (r > e) ? ca : cr; /* faster when rising */
e = c * e + (1.0 - c) * r;
env[i] = sqrt(e);
}
}
// exp(-1/tau): one-pole coefficient for a tau-sample time constant.
const decayCoeff = (tau) => Math.exp(-1 / Math.max(1, tau));
// RMS envelope: square, one-pole low-pass on the energy, then root.
function rmsEnvelope(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 = x[i] * x[i]; // instantaneous power
const c = r > e ? ca : cr; // faster when rising
e = c * e + (1 - c) * r;
env[i] = Math.sqrt(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 rms_envelope(x, attack_samples, release_samples):
"""Square, one-pole low-pass on the energy, then take the root."""
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 = xi * xi # instantaneous power
c = ca if r > e else cr # faster when rising
e = c * e + (1.0 - c) * r
env[i] = math.sqrt(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 square. The sqrt stays per-sample — it's the root of the smoothed energy, not the squared input.
#include <math.h>
void rms_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 = x[i] * x[i];
double k = (r > e) ? ka : kr;
e += k * (r - e); /* one FMA */
env[i] = sqrt(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 rms_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 = x[i] * x[i];
double c = (r > e) ? ca : cr;
e = c * e + (1.0 - c) * r;
env[i] = sqrt(e);
}
}
The two sliders map straight onto the time constants. Larger Attack (1–1000 samp) slows the rise, so the curve lags onsets; larger Release (1–4000 samp) slows the fall, so it holds level after the energy drops. Set either below the carrier period and the squared signal's ripple starts leaking through — RMS is smoother than peak, so it tolerates shorter constants before that happens.
#include <math.h>
static double decay_coeff(double tau) {
return exp(-1.0 / (tau < 1.0 ? 1.0 : tau));
}
void rms_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 = x[i] * x[i];
double c = (r > e) ? ca : cr;
e = c * e + (1.0 - c) * r;
env[i] = sqrt(e);
}
}