Offline
Centered RMS
zero-lag twinAn 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
- zero-lag
- Signal model
- single-carrier
- Reads
- RMS ≈ 0.707×
- Latency
- none (zero-lag)
- Cost
- cheap
- 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
A zero-lag RMS envelope: square each sample, average the squares, take the root. The trick is the averaging window — it is centered on each sample, drawing on equal numbers of future and past samples rather than only the ones already behind it. Because the window straddles the point it measures, the time lag is erased: the curve sits right on top of the signal instead of trailing behind it.
It reads ≈0.707·A for a sine of amplitude A, a smooth power-true outline of the swell. The catch is that it is non-causal — it needs samples from the future, so it can only run on recorded material, never a live stream.
Key terms
- Centered (symmetric) window
- An averaging window that spans an equal number of past and future samples around each point. Because it is symmetric about the sample it measures, the result has no time shift — the envelope lines up exactly with the signal underneath it.
- RMS
- Root-mean-square: square every sample, average the squares, then take the root. Squaring weights big excursions harder, so the result tracks power rather than raw amplitude — it reads 0.707·A for a sine of amplitude A.
- Non-causal
- A process that uses future samples to compute the present output. It can only run on recorded material, where the whole signal is already in hand — never on a live stream, where the future hasn't arrived yet.
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
Square every sample with x². Like rectifying, this makes every value positive — but it weights big excursions harder, which is what makes the result a measure of power rather than raw amplitude.
- Step 3Centered mean, then root
Average the squares over a window centered on each sample — half its samples before, half after — then take the square root. Because the window straddles the sample instead of trailing it, the curve has zero lag: it sits right on the swell rather than behind it.
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>
/* Centered (zero-lag) RMS: square, centered moving mean over W samples,
then square-root. The window straddles each sample (half before, half
after) and clamps at the edges, so the envelope sits right on the signal. */
void centered_rms(const double *x, double *env, int n, int W) {
if (W < 1) W = 1;
int half = W / 2;
for (int i = 0; i < n; i++) {
int lo = i - half; if (lo < 0) lo = 0;
int hi = i + half + 1; if (hi > n) hi = n;
double sum = 0.0;
for (int j = lo; j < hi; j++) sum += x[j] * x[j];
env[i] = sqrt(sum / (hi - lo));
}
}
// Centered (zero-lag) RMS: square, centered moving mean over W samples, sqrt.
// The window straddles each sample and clamps at the edges.
function centeredRms(x, W) {
W = Math.max(1, W);
const half = W >> 1;
const n = x.length;
const env = new Float64Array(n);
for (let i = 0; i < n; i++) {
const lo = Math.max(0, i - half);
const hi = Math.min(n, i + half + 1);
let sum = 0;
for (let j = lo; j < hi; j++) sum += x[j] * x[j];
env[i] = Math.sqrt(sum / (hi - lo));
}
return env;
}
import math
def centered_rms(x, W):
"""Centered (zero-lag) RMS: square, centered moving mean over W samples, sqrt.
The window straddles each sample (half before, half after) and clamps at
the edges."""
W = max(1, W)
half = W // 2
n = len(x)
env = [0.0] * n
for i in range(n):
lo = max(0, i - half)
hi = min(n, i + half + 1)
s = sum(xj * xj for xj in x[lo:hi])
env[i] = math.sqrt(s / (hi - lo))
return env
Same output, O(n) instead of O(n·W): keep a running sum of squares over the centered window. Each step adds the new sample entering on the right and subtracts the one leaving on the left, so the inner loop disappears. The divisor is the live window count (hi − lo), which shrinks at the edges exactly as the clamp does.
#include <math.h>
void centered_rms_opt(const double *restrict x, double *restrict env,
int n, int W) {
if (W < 1) W = 1;
int half = W / 2;
double sum = 0.0; /* running sum of squares over [lo, hi) */
int lo = 0, hi = 0;
for (int i = 0; i < n; i++) {
int nlo = i - half; if (nlo < 0) nlo = 0;
int nhi = i + half + 1; if (nhi > n) nhi = n;
while (hi < nhi) { sum += x[hi] * x[hi]; hi++; } /* slide right edge in */
while (lo < nlo) { sum -= x[lo] * x[lo]; lo++; } /* slide left edge out */
env[i] = sqrt(sum / (hi - lo));
}
}
Window hard-coded to W = 16 samples (the page default), so half = 8. No tuning knob — the smallest kernel for a known window size.
#include <math.h>
void centered_rms_fixed(const double *x, double *env, int n) {
const int half = 8; /* W = 16, centered: 8 before + self + 8 after */
for (int i = 0; i < n; i++) {
int lo = i - half; if (lo < 0) lo = 0;
int hi = i + half + 1; if (hi > n) hi = n;
double sum = 0.0;
for (int j = lo; j < hi; j++) sum += x[j] * x[j];
env[i] = sqrt(sum / (hi - lo));
}
}
The Window slider (4–512 samples) sets W directly. A wider window averages more and smooths harder; a narrower one tracks faster but lets carrier ripple through. The level lands at ≈0.707× peak for a sine regardless of width. Because the window is centered, the envelope has zero lag at every setting — widen it and it stays time-aligned on the signal instead of dragging behind.
#include <math.h>
void centered_rms_ctl(const double *x, double *env, int n,
int W) { /* slider: 4 .. 512 samples */
if (W < 1) W = 1;
int half = W / 2;
for (int i = 0; i < n; i++) {
int lo = i - half; if (lo < 0) lo = 0;
int hi = i + half + 1; if (hi > n) hi = n;
double sum = 0.0;
for (int j = lo; j < hi; j++) sum += x[j] * x[j];
env[i] = sqrt(sum / (hi - lo));
}
}