Skip to main content

Master Limiter

Added in 5.8.0

When several sounds play at the same time their waveforms add up. Five samples at full volume sum to roughly five times full scale, and the Web Audio destination hard-clips everything above that ceiling. You hear it as crackle or distortion, most of all on sounds with a sharp attack such as piano notes, drum hits or gunfire.

The master limiter holds those peaks back instead of letting them clip. It sits last in the master chain, after volume and panning, so it catches everything on the way out:

sound → pan → gain → master gain → master spatial panner → master stereo panner → [limiter] → output

The master spatial panner is only there while setMasterSpatialPosition is in use.

The limiter is off by default, so upgrading to 5.8.0 never changed how an existing project sounds. Turn it on when you mix several sounds at once.

Try it​

Play a stack of identical notes with the limiter off and then on. Identical samples add up coherently, so this reaches the clipping ceiling quickly.

Try it

Overload the output, then limit it

Idle
6
112
Limiteroff
Gain reduction0.0 dB

Set 8 or more notes and play with the limiter off. Then turn it on and play again. The level stays about the same, but the harsh edge on the attack is gone.

Code
Your clicks show up here as soundhub calls

Enabling it​

Set it once when you create the hub:

import { SoundHub } from 'soundhub';

const soundHub = new SoundHub({
overlap: true,
masterLimiter: true, // prevents clipping when sounds overlap
});

Or toggle it at runtime with setMasterLimiter. This only rewires the output chain, so playback is not interrupted:

soundHub.setMasterLimiter(true);
soundHub.isMasterLimiterEnabled(); // true

Tuning it​

The limiter is a DynamicsCompressorNode set up as a brick-wall limiter rather than a compressor:

ParameterValueWhy
threshold-3 dBFSLeaves a little headroom below the ceiling
knee0Hard knee, so it limits rather than compresses
ratio20Effectively a brick wall
attack0.003 sFast enough to catch note transients
release0.25 sRecovers without pumping

Anything below the threshold passes through untouched, so quiet material is unaffected.

If you need different behaviour, getMasterLimiterNode gives you the live DynamicsCompressorNode:

const limiter = soundHub.getMasterLimiterNode();

if (limiter) {
limiter.threshold.value = -6; // start limiting earlier
limiter.release.value = 0.4; // let it recover more slowly

console.log(limiter.reduction); // current gain reduction in dB
}

Reading reduction is the easiest way to check whether the limiter is doing anything: it stays at 0 while you are under the threshold and goes negative as peaks are held back.

Audio you generate yourself​

The limiter only sees what passes through the master chain. Sound you create with an oscillator, a MediaElementSource or an AudioWorklet and connect straight to context.destination bypasses it completely, along with master volume, mute and panning.

Connect it to getMasterInput() instead:

const context = soundHub.getContext();

const osc = context.createOscillator();
const gain = context.createGain();

osc.connect(gain);
gain.connect(soundHub.getMasterInput()); // not context.destination

osc.start();

Synthesized sources are usually much hotter than recorded samples. A sawtooth at full amplitude peaks at 0 dBFS while a typical sample peaks around -20 dBFS, so an untamed oscillator can be ten times louder than everything else and clip on its own.

When to use it​

SituationRecommendation
A single background trackNot needed
Playing a sound the user triggers repeatedlyWorth enabling
A playable instrument, several notes at onceEnable
A game scene with many overlapping effectsEnable
You already mix at low per-sound volumesOptional, it will rarely engage

The limiter catches what you did not plan for. If you know you will play ten sounds at once, lowering the individual volumes still gives the cleanest result.

Good to know​

  • An analyser on getMasterOutput() sits before the limiter, so it shows the mix before limiting.
  • Since 6.3.2, toggling the limiter (and any other rebuild of the master chain) no longer disconnects nodes you connected to getMasterOutput().
  • Turning the limiter off discards its node. Turning it on again creates a fresh one with the defaults above, so settings made through getMasterLimiterNode() have to be applied again.

See also​