Reverb plugins usually ship with impulse responses: short recordings of a real space that the audio gets convolved with. They are also the heavy part. When I added a reverb tool to SongUp AI, I wanted it to load fast and never touch a server. So the tool ships no impulse files at all. It generates them in the browser, on the spot, each time you pick a room.
It turned out to take about 20 lines of JavaScript.
What a convolution reverb needs
The Web Audio API already has a ConvolverNode. Give it an AudioBuffer as the impulse response and it does the heavy maths for you. The only question is where the buffer comes from.
A real room's response is, roughly, a burst of reflections that gets denser and quieter until it blurs into noise. So decaying noise is a surprisingly good stand-in, as long as you get three details right.
1. Decay: fall by 60 dB on purpose
Reverb time is normally quoted as RT60, the time a sound takes to fall by 60 dB. 60 dB is a factor of 1000 in amplitude, which gives a clean exponential:
const tail = Math.max(1, Math.round(decay * sampleRate));
const k = Math.log(1000) / tail; // -60 dB at the end of the tail
// sample i is multiplied by Math.exp(-k * i)
Now "Decay: 2.6 s" in the UI means what it says.
2. Darkness: high frequencies die first
Plain white noise sounds like a hissy cave. In real rooms, air and soft surfaces eat the highs, so the tail gets darker. A one-pole low-pass inside the loop gets most of the way there:
const a = Math.exp((-2 * Math.PI * damp) / sampleRate);
let lp = 0;
for (let i = 0; i < tail; i++) {
lp = a * lp + (1 - a) * (Math.random() * 2 - 1);
data[pre + i] = lp * Math.exp(-k * i);
}
Each preset is just a few numbers. Small Room is 0.5 s with the low-pass at 6 kHz. Cathedral is 6.5 s at 2.6 kHz. Vocal Plate is 1.8 s at 9 kHz, with no pre-delay, because a plate is bright and dense and has no walls to travel to.
3. Pre-delay: the size cue
The gap before the first reflection tells your ear how far away the walls are. It is only silence at the start of the buffer: 5 ms for a small room, 45 ms for a cathedral. Cheap, and it does more for the sense of space than you would expect.
Each stereo channel gets its own random noise, so the two sides differ, which is what makes the reverb feel wide rather than centred.
The bug worth warning you about
ConvolverNode has a normalize flag, on by default. I turned it off and normalised the impulse to unit energy myself, so the wet level stays about the same as the dry level whatever the decay. If you don't control this, the Decay slider can end up acting like a volume slider too.
The dry and wet signals then go through an equal-power blend, so moving Mix does not make the track dip in the middle:
const dry = Math.cos((mix * Math.PI) / 2);
const wet = Math.sin((mix * Math.PI) / 2);
A limiter sits after the mix to stop loud parts clipping.
Exporting without a server
Preview uses a live AudioContext. The download uses an OfflineAudioContext with the same graph, made longer than the input so the tail can ring out instead of being cut off. The file never leaves the device.
What it does not do
Noise is not a measured room. If you need the exact sound of a specific hall or a vintage plate, a sampled impulse response is still better. For giving a dry vocal or a voice note some space, generated noise does the job, and there is nothing to download.
You can try the result here, free with a free account, with six spaces, Mix and Decay sliders and live preview: Add Reverb to Audio Online.
I build SongUp AI. Drafted with AI assistance and checked against the tool's source code.
Top comments (0)