Real-time
Moving avg
rectified meanAn 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
- low-latency
- Signal model
- single-carrier
- Reads
- mean-rect ≈ 0.637×
- Latency
- ½ window
- 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
Linear-phase smoothing with perfectly predictable behavior. Rectify with |x|, then average over a fixed window — a boxcar FIR. Every sample in the window counts equally, so the ripple and the response are completely deterministic.
The price is latency equal to half the window, and it reports the mean-rectified level (≈0.637·A for a sine of amplitude A). A wider window is smoother and laggier; there is no attack/release asymmetry.
Key terms
- Boxcar / moving average (FIR)
- Averaging a fixed window of samples where every sample counts equally — a finite-impulse-response filter whose output is just the mean of the last N values.
- Linear phase
- Every frequency is delayed by the same amount — half the window — so the curve's shape isn't distorted, just shifted. Nothing smears or rings; the envelope arrives late but faithful.
- Mean-rectified level
- The average of |x|. For a sine of amplitude A this settles at ≈0.637·A — the mean of a rectified sine, not its crest.
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 an average settles toward the amplitude instead of cancelling back to zero.
- Step 3Boxcar average
Slide a fixed-width window across |x| and average it. Every sample in the window counts equally, so the output is an evenly smoothed mean that lags by half the window — the moving-average 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>
/* Moving-average envelope: rectify, then a trailing boxcar of width W.
acc holds the running sum of the last W rectified samples; during
warm-up (i < W) fewer samples are in, so divide by (i + 1). */
void movavg_envelope(const double *x, double *env, int n, int W) {
if (W < 1) W = 1;
double acc = 0.0;
for (int i = 0; i < n; i++) {
acc += fabs(x[i]);
if (i >= W) acc -= fabs(x[i - W]); /* drop the sample leaving the window */
int count = (i + 1 < W) ? (i + 1) : W;
env[i] = acc / count;
}
}
// Moving-average envelope: rectify, then a trailing boxcar of width W.
// acc is the running sum of the last W rectified samples; warm-up divides
// by the number of samples seen so far, min(i + 1, W).
function movavgEnvelope(x, W) {
W = Math.max(1, Math.round(W));
const env = new Float64Array(x.length);
let acc = 0;
for (let i = 0; i < x.length; i++) {
acc += Math.abs(x[i]);
if (i >= W) acc -= Math.abs(x[i - W]); // drop the sample leaving the window
env[i] = acc / Math.min(i + 1, W);
}
return env;
}
def movavg_envelope(x, W):
"""Rectify, then a trailing boxcar of width W.
acc is the running sum of the last W rectified samples; during warm-up
divide by the number of samples seen so far, min(i + 1, W)."""
W = max(1, round(W))
env = [0.0] * len(x)
acc = 0.0
for i, xi in enumerate(x):
acc += abs(xi)
if i >= W:
acc -= abs(x[i - W]) # drop the sample leaving the window
env[i] = acc / min(i + 1, W)
return env
The running sum IS the optimization. A naive boxcar re-adds W samples at every output — O(n·W). Here acc carries the window's sum forward: add the entering sample, subtract the one that left, and the per-sample work is constant — O(n) regardless of window width. `restrict` lets the compiler keep acc in a register since x and env can't alias.
#include <math.h>
void movavg_envelope_opt(const double *restrict x, double *restrict env,
int n, int W) {
if (W < 1) W = 1;
double acc = 0.0;
for (int i = 0; i < n; i++) {
acc += fabs(x[i]);
if (i >= W) acc -= fabs(x[i - W]);
/* divide by W once warm; warm-up uses the running count (i + 1) */
env[i] = acc / (i + 1 < W ? i + 1 : W);
}
}
Window hard-coded to 16 samples (the page default). No tuning knobs — the division by 16 is a constant the compiler can fold, and the warm-up branch still averages fewer samples for the first 16 outputs.
#include <math.h>
#define W 16 /* fixed window, in samples */
void movavg_envelope_fixed(const double *x, double *env, int n) {
double acc = 0.0;
for (int i = 0; i < n; i++) {
acc += fabs(x[i]);
if (i >= W) acc -= fabs(x[i - W]);
env[i] = acc / (i + 1 < W ? i + 1 : W);
}
}
W maps straight onto the page's Window slider (2–512 samples). Wider is smoother and slower: the output is the mean of more samples, so ripple shrinks and the curve lags transients by half the window. Narrower tracks faster but lets more of the carrier ripple through.
#include <math.h>
void movavg_envelope_ctl(const double *x, double *env, int n,
int W) { /* slider: 2 .. 512 samples */
if (W < 1) W = 1;
double acc = 0.0;
for (int i = 0; i < n; i++) {
acc += fabs(x[i]);
if (i >= W) acc -= fabs(x[i - W]);
env[i] = acc / (i + 1 < W ? i + 1 : W);
}
}