Try it in the browser: Loudness meter and normalizer. Runs on this package, nothing is uploaded.
Dynamics processing — compressor, limiter, gate, expander, de-limiter, de-esser, ducker, softclip, compand, multiband. The family includes envelope-driven gain control, lookahead limiting, waveshaping and whole-buffer level correction. Part of audiojs.
| Kind | Gain function | Typical use | |
|---|---|---|---|
| compressor | envelope | soft-knee above threshold | leveling vocals, mix glue |
| limiter | lookahead | brickwall at ceiling | master bus, peak control |
| gate | envelope | hard cut below threshold | silence between phrases |
| expander | envelope | gentle below-threshold reduction | soft gating, noise bed shaping |
| unlimit | dual envelope | transient-gated upward expansion | de-limiting, crest restoration |
| deesser | sidechain | sibilance band over voice body, cut within a range | harsh 's' in voice |
| ducker | ext. sidechain | compressor keyed by side signal | music-under-voice, podcast |
| softclip | waveshaper | static transfer curve | gentle peak limiting + coloration |
| compand | envelope | piecewise-linear transfer | SoX-style multi-segment |
| multiband | envelope × N bands | LR split + per-band up/down compression | mastering glue, OTT-style upward+downward |
npm install @audio/dynamics
import { compressor, limiter } from '@audio/dynamics'
const sampleRate = 48000
const samples = Float32Array.from({ length: 4800 }, (_, i) =>
0.8 * Math.sin(2 * Math.PI * 220 * i / sampleRate))
const opts = { sampleRate, threshold: -18, ratio: 4, attack: 5, release: 100 }
const compressed = compressor(samples, opts)
const limited = limiter(compressed, { sampleRate, ceiling: -1, lookahead: 5 })
console.log(limited.length) // 4800; samples and compressed are preserved
// Each writer owns its history. Keep it for successive chunks, then flush once.
const write = compressor(opts)
const blocks = [write(samples.subarray(0, 256)), write(samples.subarray(256)), write()]
console.log(blocks.reduce((n, block) => n + block.length, 0)) // 4800For one processor: npm install @audio/dynamics-compressor, then import compressor from '@audio/dynamics-compressor'. The umbrella re-exports the same functions and types. Existing CompressorOpts, GateOpts and other legacy option names remain available alongside the leaf names (CompressorOptions, GateOptions, etc.).
| Functions | Buffer ownership / streaming | Sample-rate option |
|---|---|---|
compressor, limiter, gate, expander, unlimit, deesser, ducker, compand, opto, fet, vca, varimu |
Batch calls return a new buffer. Calling with options returns a writer; keep it across chunks and call with no arguments to flush. | sampleRate |
softclip |
Returns a new buffer. Its writer buffers until flush when oversample > 1. |
fs |
transientShaper |
Mutates and returns the input. Reuse the same options object across chunks; a fresh object resets the envelopes. | fs |
multiband, leveler |
Mutate and return the input. Whole-buffer processing; no writer form. | fs |
envelope |
Returns a stateful sample → level function. Create another follower to reset. | sampleRate |
PCM is mono Float32Array; process channels with separate state/writers. Sample rate defaults to 44100 Hz. Gains/thresholds are dB and attack/release/lookahead are milliseconds unless stated otherwise: softclip.drive and transient-shaper gains are linear, while leveler.frame is seconds. Do not substitute fs for sampleRate indiscriminately; only some processors accept that alias.
Batch compression and a fresh writer fed the same signal produce the same samples. Lookahead writers can return fewer samples until flush; concatenate every returned block, including the final flush. An empty chunk is a zero-length write, not a flush. Treat flush as the end of a signal and create a new writer to reset. Options are construction settings, not a live automation interface.
The leaf /audio exports are host processor factories with their own parameter metadata. Some controls restart their processor, and some factories require a whole render (streaming: false); check those limits before using an AudioWorklet adapter.
The common detector for the envelope-driven processors is a branching one-pole follower with separate attack/release time constants, peak or RMS detection.
import { envelope } from '@audio/dynamics'
let follow = envelope({ attack: 5, release: 100, detector: 'peak' })
let level = []
for (let x of samples) level.push(follow(x))| Param | Default | |
|---|---|---|
sampleRate |
44100 |
— |
attack |
5 |
ms |
release |
50 |
ms |
detector |
'peak' |
'peak' or 'rms' |
rmsWindow |
256 |
samples, for RMS detector |
Feed-forward soft-knee downward compressor, as Giannoulis, Massberg & Reiss recommend: level in dB → quadratic soft-knee gain curve → gain reduction smoothed in the log domain by the smooth decoupled peak detector → linear gain applied to input. attack and release are the time constants of the gain reduction itself (1 − 1/e), at any depth of compression; the first sample over the threshold is already reduced.
Downward compression (above threshold, reduces gain) is one half of the canonical four-quadrant dynamics taxonomy — downward/upward compression, downward/upward expansion (Giannoulis, Massberg & Reiss 2012; Izhaki, Mixing Audio). Setting upThreshold engages the other compression half: upward compression lifts quiet passages toward the threshold instead of squashing loud ones — the "OTT up" half popularized by Xfer OTT. Both curves read the same envelope and sum in the dB domain, so a single compressor call can glue loud material down and lift quiet material up at once.
import { compressor } from '@audio/dynamics'
compressor(data, { threshold: -18, ratio: 4 })
compressor(data, { threshold: -24, ratio: 2, knee: 12, attack: 10, release: 200, makeup: 6 })
// upward + downward together (OTT-style): lift quiet passages, squash loud ones
compressor(data, { threshold: -18, ratio: 4, upThreshold: -40, upRatio: 2, upRange: 12 })| Param | Default | |
|---|---|---|
threshold |
-20 |
dB |
ratio |
4 |
— |
knee |
6 |
dB (soft-knee width) |
attack |
5 |
ms |
release |
100 |
ms |
makeup |
0 |
dB |
depth |
1 |
scales the summed up+down gain before makeup (OTT "Depth" macro; 0 = identity) |
upThreshold |
null |
dB; null disables upward compression |
upRatio |
2 |
— (1 is a mathematical no-op) |
upKnee |
6 |
dB |
upRange |
12 |
dB, max upward lift — without a ceiling, silence would take unbounded gain |
Use when: vocals, bass, drum bus, mix glue; add upThreshold for OTT-style up+down "aggressive" glue.
Not for: peak control at the master bus — use limiter. Transparent loudness shaping — use compand with gentle slope.
Lookahead brickwall limiter. The gain each sample needs, its sliding minimum over the lookahead span and that minimum's moving average: the gain ramps down across lookahead ms into a peak, never stepping, and still covers every sample in transit; exponential release after it passes.
import { limiter } from '@audio/dynamics'
limiter(data, { ceiling: -0.3 })
limiter(data, { ceiling: -1, lookahead: 10, release: 100 })| Param | Default | |
|---|---|---|
ceiling |
-0.3 |
dB (brickwall) |
lookahead |
5 |
ms (introduces delay) |
release |
50 |
ms |
Use when: sample-peak control at the master bus. This limiter does not measure or constrain reconstructed inter-sample peaks.
Not for: musical dynamics shaping — use compressor. Low-latency paths — use softclip.
Noise gate with hysteresis, hold-then-close logic and look-ahead. Opens above threshold, closes only below closeThreshold (hysteresis prevents chatter around a single threshold); below it, signal is attenuated by range dB. A hold timer keeps the gate open after a drop-out; attack/release smooth the gain transitions. lookahead runs detection ahead of emission so the gate is already opening when a transient reaches the output — batch calls stay sample-aligned (no silence prefix, no dropped tail); block hosts get the delay declared as atom latency.
import { gate } from '@audio/dynamics'
gate(data, { threshold: -40 })
gate(data, { threshold: -35, range: -80, hold: 20, attack: 1, release: 150, lookahead: 5 })| Param | Default | |
|---|---|---|
threshold |
-40 |
dB, open above |
closeThreshold |
threshold − 6 |
dB, close below (hysteresis) |
range |
-60 |
dB attenuation when closed |
hold |
10 |
ms |
attack |
0.1 |
ms (opening) |
release |
100 |
ms (closing) |
lookahead |
0 |
ms, detection leads emission |
Use when: drum mics, voice dialogue with ambient noise, removing hiss between phrases.
Not for: subtle low-level reduction — use expander.
Downward expander (mode: 'downward', default) — a softer gate. Below threshold, gain is reduced by (threshold − level) × (ratio − 1) dB, clamped at range.
mode: 'upward' switches to upward expansion — the de-compression complement, raising gain above threshold instead of cutting it below. Classical substrate for de-limiting: transient-aware upward expansion restores crest factor a brickwall limiter (or an over-eager mix bus compressor) flattened. Same four-quadrant taxonomy as compressor's upward mode (Giannoulis/Reiss; Izhaki, Mixing Audio).
import { expander } from '@audio/dynamics'
expander(data, { threshold: -30, ratio: 2 })
// de-limiting: expand transients back out above threshold
expander(data, { mode: 'upward', threshold: -20, ratio: 1.5, range: 20 })| Param | Default | |
|---|---|---|
mode |
'downward' |
'downward' | 'upward' |
threshold |
-30 |
dB |
ratio |
2 |
— |
knee |
6 |
dB |
range |
-40 (downward) / 20 (upward) |
dB, max reduction (downward, negative) or max lift (upward, positive) |
attack |
5 |
ms |
release |
50 |
ms |
Use when: gentle noise-floor suppression without the abruptness of a gate (downward); restoring dynamics to over-compressed or over-limited material (upward).
Not for: hard removal of sound between phrases — use gate.
De-limiter. iZotope Ozone 12's "Unlimiter" (Sept 2025) created the de-limiting category with a trained ML model; this atom is the classical counterpart — transient-synchronous upward expansion, restoring the crest factor a brickwall limiter (or an over-eager bus compressor) flattened. Program-adaptive upward expansion gated to transients, not level — one more cell in the four-quadrant dynamics taxonomy (Giannoulis, Massberg & Reiss 2012, JAES 60(6); Izhaki, Mixing Audio), built on expander's upwardExpanderGain curve.
A fast envelope (fastAttack/fastRelease, near-instant) and a slow envelope (slowAttack/slowRelease, sluggish) both track the input; their gap in dB — transientness — rises sharply on attacks and sits near zero on sustained material. Gain lift follows transientness, not absolute level: an absolute-level upward expander would pump sustains; gating on the fast/slow gap instead is what makes this a de-limiter rather than a leveler.
import { unlimit } from '@audio/dynamics'
unlimit(data, { amount: 9, drive: 2 }) // deliberate restoration
unlimit(data, { amount: 9, drive: 2, ceiling: -1 }) // guard restored peaks at -1 dBFS| Param | Default | |
|---|---|---|
amount |
6 |
dB, max crest restoration (range 0–18) |
drive |
1 |
scales the deficit-driven restoration (1 = restore attacks to crestTarget); in adaptive: false mode, dB of lift per dB of transientness |
adaptive |
true |
deficit mode (see below); false = raw proportional transient-following (a transient exaggerator, for manual sound design) |
crestTarget |
10 |
dB of transientness a healthy attack is expected to show; flattened attacks get lifted by what they're missing |
ceiling |
null |
dBFS; post guard so restored peaks don't exceed it. null (default): peaks may exceed 0 dBFS by design (float domain) |
fastAttack |
0.5 |
ms |
fastRelease |
20 |
ms |
slowAttack |
20 |
ms |
slowRelease |
200 |
ms |
The default mode lifts by transient deficit, not transient presence — the inverse-limiter insight: a brickwall limiter's fingerprint is attacks that are too small (3–8 dB of fast-over-slow transientness where healthy program shows 12–25 dB), so each onset gets back crestTarget − measured dB, and a naturally healthy attack gets structurally zero lift. Safety on dynamic material is a property of the curve, not a timid default: measured on the test fixture, defaults change never-limited program by ≤ 0.2 dB RMS while amount: 9 recovers ~4.5 dB of crest from a 9 dB-crushed brickwall (see tests). Three gates make that separation robust — peak-hold (deficit is judged against an attack's peak transientness, not its rise samples), a 10 ms attack window (a decaying tail keeps the fast envelope above the slow one for its whole length; a decay is not an onset), and a ~3 ms confirmation ramp (a healthy attack outruns crestTarget in ~1.5 ms, collapsing its own deficit before lift confirms; a limiter-flattened plateau is still standing).
Honest scope: this restores dynamics/crest — it cannot recover information a clipper already destroyed (pair with @audio/denoise-declip for that), and it does not un-mix limiter pumping artifacts baked into the waveform's history. Over-driving amount/drive invents transients that were never there. v1 is zero-latency with no lookahead — it reacts to a transient already underway, it cannot anticipate one; lookahead attack-anticipation is a future option.
Use when: restoring life to over-limited masters, streaming-loudness-flattened stems, squashed dialogue or game audio.
Not for: recovering clipped/distorted peaks — use a declipper. Undoing audible limiter pumping — remix from an earlier, unlimited stage if one exists.
Sibilance reduction. An 's' is told by its shape, not its level: the sibilance band (a bandpass at fc, Q) is measured against the voice body (a low-pass an octave under fc), in dB, as the dbx 902 compares its high band with the full band. The same 's' is caught on a quiet recording and a loud one, and a cymbal in a mix, which sits over the mix's body, is left alone. How far the band rises over threshold sets the cut, held within range, the Range of the dbx 902, FabFilter Pro-DS and Waves Sibilance: past about 6 dB an 's' turns into a lisp. A pause is measured against the voice's running level, so hiss between words is not taken for an 's'. mode applies the cut: broadband (default) turns the whole sound down while the 's' lasts; band is a peaking EQ at fc, so only the sibilance band moves.
import { deesser } from '@audio/dynamics'
deesser(data) // at any recording level
deesser(data, { mode: 'band', fc: 7500, Q: 1 }) // only the sibilance band moves
deesser(data, { threshold: -3, range: -8 }) // softer esses too, deeper| Param | Default | |
|---|---|---|
mode |
'broadband' |
'broadband' | 'band' |
fc |
6500 |
Hz, sibilance center: the band watched and, in band mode, cut; the body is under fc / 2 (freq still accepted) |
Q |
2 |
width of that band; 1.4 in band mode (q still accepted) |
threshold |
0 |
dB of the sibilance band over the voice body where the cut starts; not a level |
ratio |
4 |
— |
range |
-6 |
dB, the deepest cut |
knee |
6 |
dB |
attack |
1 |
ms |
release |
15 |
ms |
node scripts/deesser.js runs it over speech, singing and music and reads, per 10 ms frame, how far the 4–10 kHz band and the 0.3–4 kHz voice body moved: on 's' frames (active, half their energy above 3.5 kHz) the cut, elsewhere the share of frames moved by more than 1 dB. The defaults were chosen on 504 VoiceBank training utterances of 28 speakers and 10 narrations (SET=train); below, the VoiceBank test set (824 utterances, 2 other speakers) measured once, 10 other narrations (spoken Wikipedia, 60 s each), VocalSet:
| 's' frames, 4–10 kHz cut: median · 90th pct · most | voice frames moved > 1 dB | pauses moved > 1 dB | |
|---|---|---|---|
| VoiceBank test, broadband | 0.6 · 4.4 · 6.0 dB | 1.1% | 0.2% |
| narrations, broadband | 2.1 · 6.0 · 6.0 dB | 1.2% | 0.4% |
| sung, broadband | 3.6 · 6.0 · 6.0 dB | 0.4% | 0.7% |
| VoiceBank test, band | 0.6 · 3.3 · 5.7 dB | 0.9% | 0.1% |
| narrations, band | 1.7 · 3.9 · 5.7 dB | 1.1% | 0.3% |
| sung, band | 2.6 · 4.2 · 5.1 dB | 0.4% | 0.6% |
Normalized to −16 LUFS first, as the editor's recipes do, every figure is the same. Music ("Vibe Ace", Brahms, the Nutcracker, a trumpet): no frame moved by 1 dB in either mode. A ride cymbal 18, 12 and 6 dB under "Vibe Ace", the mix at −16 LUFS: its 4–10 kHz band cut by a median 0 dB (90th percentile 0, 0 and 0.5 dB).
0.2.7 (a threshold on the band's own level, its cut unbounded) did next to nothing at its defaults on these files at their own level (the narrations sit between −41 and −11 LUFS), and too much once they were loud: at −16 LUFS with the editor's "De-ess a voice" settings ({ mode: 'band', fc: 7500, Q: 1, threshold: -35 }) it cut the narrations' 's' frames by a median 5.7 dB, 10.9 at the 90th percentile and up to 15.9, moved 23 % of their voice frames and 22 % of Brahms' and the trumpet's, and cut the ride 12 dB under "Vibe Ace" by a median 5.4 dB. { mode: 'band', fc: 7500, Q: 1 } now: 1.5 · 4.6 · 5.6 dB, 1.0 %, none, 0 dB.
Use when: harsh 's' / 'sh' in a voice, bright vocal takes; mode: 'band' when the voice sits with program that must not pump.
Not for: broadband brightness — use an EQ. Generic compression — use compressor.
External-sidechain compressor. Main signal's gain tracks the level of a separate side signal.
import { ducker } from '@audio/dynamics'
// batch
let podcast = ducker(music, voice, { threshold: -30, range: -12 })
// streaming — callable takes (main, side); call with no args to flush
let duck = ducker({ threshold: -30, range: -15 })
let out1 = duck(musicBlock1, voiceBlock1)
let out2 = duck(musicBlock2, voiceBlock2)
let tail = duck()| Param | Default | |
|---|---|---|
threshold |
-30 |
dB (on side level) |
ratio |
4 |
— |
knee |
6 |
dB |
range |
-24 |
dB, max reduction |
attack |
20 |
ms |
release |
300 |
ms |
Use when: music-under-voice podcasts, dialogue ducking, sidechain-pumped mixes.
Not for: sidechain from the same signal — use compressor.
Static waveshaping — no time state, no pumping. Maps input through a fixed transfer curve; peaks saturate smoothly, introducing controlled harmonic content.
Hard/high-drive clipping generates harmonics above Nyquist that fold back as audible aliasing. oversample (1/2/4/8, default 1) runs the transfer at N× rate and decimates back down through a windowed-sinc anti-alias filter, same technique as @audio/saturate's oversampled shapers — oversample: 1 is the exact non-oversampled path (no resampling, zero cost).
import { softclip } from '@audio/dynamics'
softclip(data, { curve: 'tanh', drive: 1.5 })
softclip(data, { curve: 'cubic', drive: 2, ceiling: 0.9 })
softclip(data, { curve: 'hard', drive: 4, oversample: 4, fs: 44100 }) // clean high-drive clip| Param | Default | |
|---|---|---|
curve |
'tanh' |
'tanh', 'atan', 'cubic', 'sin', 'hard' |
drive |
1 |
input pre-gain |
ceiling |
1 |
output asymptote |
oversample |
1 |
1, 2, 4, 8 — anti-aliased oversampling |
fs |
44100 |
Hz, sample rate (only used when oversample > 1) |
Use when: gentle peak control with musical saturation, avoiding pumping artifacts of a limiter, lo-fi character; oversample for hard/high-drive clipping that must stay alias-free.
Not for: transparent true-peak safety — use limiter. Clean gain reduction — use compressor.
SoX-style multi-segment compander. Arbitrary piecewise-linear transfer in dB unifies compression, expansion, and gating under one curve — points below the identity line compress; above, they expand.
import { compand } from '@audio/dynamics'
// Default: compress above -20 dB
compand(data)
// Broadcast leveler: lift -40..-20 dB, compress above -10 dB
compand(data, {
points: [[-90, -90], [-40, -30], [-20, -18], [-10, -10], [0, -4]],
attack: 20, release: 500
})| Param | Default | |
|---|---|---|
points |
[[-90,-90],[-60,-60],[-20,-20],[0,-8]] |
[[inDb, outDb], ...] |
attack |
5 |
ms |
release |
200 |
ms |
Use when: broadcast leveling, speech normalization, any time a single compressor's fixed ratio is too rigid.
Not for: simple threshold compression — use compressor.
Multiband compressor — Linkwitz-Riley crossover split, an independent compressor per band (upward half included), flat sum by construction (SoX mcompand class). The manifest (multiband/audio) is a 3-band "one-knob" mastering stage — one shared setting across low/mid/high, split at low/high. The kernel (multiband(data, opts)) takes N-1 crossover points and per-band settings directly, for full control; every bands entry is spread straight into compressor(), so upward compression and depth are already there per band.
import { multiband } from '@audio/dynamics'
// one-knob: shared setting across 3 bands split at 200/2000 Hz
multiband(data, { freqs: [200, 2000], bands: { threshold: -24, ratio: 3 } })
// per-band settings, N bands (mutates data in place)
multiband(data, {
freqs: [400, 4000],
bands: [
{ threshold: -24, ratio: 3 }, // low
{ threshold: -20, ratio: 4, makeup: 2 }, // mid
null, // high: pass through uncompressed
],
})OTT-class upward+downward multiband — Xfer OTT's "upward + downward compression on 3 bands" recipe, reproduced with this atom's upThreshold/upRatio/depth:
let depth = 1 // OTT's "Depth" macro — 0 is a transparent pass, 1 is full effect, up to 2 overshoots it
multiband(data, {
freqs: [88.3, 2500], // OTT's own crossover points
bands: [
{ threshold: -24, ratio: 4, upThreshold: -30, upRatio: 2, attack: 2, release: 35, depth }, // low
{ threshold: -24, ratio: 4, upThreshold: -30, upRatio: 2, attack: 5, release: 60, depth }, // mid
{ threshold: -24, ratio: 4, upThreshold: -30, upRatio: 2, attack: 2, release: 35, depth }, // high
],
})| Param | Default | |
|---|---|---|
freqs |
[200, 2000] |
Hz, N-1 crossover points for N bands |
bands |
— | per-band {threshold, ratio, knee, attack, release, makeup, upThreshold, upRatio, upKnee, upRange, depth}, or one object shared by all bands; null passes a band through uncompressed |
order |
4 |
Linkwitz-Riley crossover order (2, 4, 8) |
fs |
44100 |
Hz |
Manifest params (3-band one-knob form): low, high, threshold, ratio, upThreshold, upRatio, depth, attack, release, makeup.
Use when: mastering-stage glue across the spectrum; OTT-style "upward + downward everywhere" aggressive multiband; taming one band without touching others.
Not for: single-band dynamics — use compressor directly.
| Export | Behavior and detailed options |
|---|---|
transientShaper |
Attack/sustain gain shaping; state continues on the reused options object. |
opto |
Optical-style compression, RMS detection and program-dependent release. |
fet |
FET-style compression, fast peak detection. |
vca |
VCA-style compression, feed-forward peak detection and a firm knee. |
varimu |
Variable-mu-style compression, level-dependent ratio. |
leveler |
Dialogue gain riding, whole-buffer analysis and smoothing. |
These compressor models describe gain-control behavior; they do not model a hardware unit's full circuit or coloration.
The umbrella also exposes pure dB gain-curve helpers: compressorGain(levelDb, threshold, ratio, kneeDb), upwardGain(levelDb, threshold, ratio, kneeDb, rangeDb?), upwardExpanderGain(levelDb, threshold, ratio, kneeDb, rangeDb), and unlimitGain(fastDb, slowDb, amount, drive). They return gain in dB and do not modify audio. See the corresponding leaf declarations for parameter details.
- denoise — umbrella for everything noise; its
gate/deesserare seconds-unit adapters over this package (2026-07 near-dupe merge) - filter — biquads for deesser sidechain
- effect — modulation effects
- stretch — sibling package
- Giannoulis, D., Massberg, M. & Reiss, J.D. (2012). "Digital dynamic range compressor design — a tutorial and analysis." JAES, 60(6).
- Izhaki, R. Mixing Audio: Concepts, Practices and Tools. Focal Press / Routledge. Four-quadrant dynamics taxonomy — downward/upward compression, downward/upward expansion.
- Zölzer, U. (ed., 2011). DAFX — Digital Audio Effects (2nd ed.), chapter on dynamics processing.
- Reiss, J.D. & McPherson, A. (2014). Audio Effects — Theory, Implementation and Application, Ch. 6.
- Bristow-Johnson, R. (2005). "Audio EQ Cookbook." (RBJ biquad formulae, used in deesser sidechain.)
- dbx Professional Products (1996). 902 de-Esser owner's manual (18-2015-B): sibilance detected in dB of the high band against the full band, "regardless of variations in signal levels"; Range 0–20 dB, past its normal region an 's' is "swallowed".
- Sonnox. Oxford SuprEsser user guide, §3.3.2: auto threshold that follows the signal outside the band, with a 24 dB window.
- FabFilter. Pro-DS help: Threshold, Range ("scales the detected gain reduction so that it stays within a desired range"), Wide Band / Split Band. Waves. Sibilance user guide: Range 0 to −48 dB.
- SoX manual —
compand(piecewise-linear compander semantics).