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.
Overload the output, then limit it
IdleSet 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.
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:
| Parameter | Value | Why |
|---|---|---|
threshold | -3 dBFS | Leaves a little headroom below the ceiling |
knee | 0 | Hard knee, so it limits rather than compresses |
ratio | 20 | Effectively a brick wall |
attack | 0.003 s | Fast enough to catch note transients |
release | 0.25 s | Recovers 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
| Situation | Recommendation |
|---|---|
| A single background track | Not needed |
| Playing a sound the user triggers repeatedly | Worth enabling |
| A playable instrument, several notes at once | Enable |
| A game scene with many overlapping effects | Enable |
| You already mix at low per-sound volumes | Optional, 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
setMasterLimiter: turn it on or off at runtime.isMasterLimiterEnabled: check whether it is on.getMasterLimiterNode: fine-tune it or read its gain reduction.soundHubConfig: themasterLimiteroption.