Reverb Without a Recording: Making Up a Room in Code
Short answer: The browser's reverb needs a short piece of audio that describes a room. You don't have to record one or download one. Fill a buffer with random noise that fades out, hand it to the reverb, and you get a soft, believable space with no file at all.
The problem
My app has no audio files. Every sound is built in the browser while it plays, which I wrote about in there are no audio files. Reverb looked like the one place where I would need a file after all.
I couldn't skip it either. Without reverb the sounds were dry and close, as if they were happening inside my head instead of somewhere around me.
What the browser's reverb wants
Web Audio has a ready-made reverb called ConvolverNode. MDN says it "performs a Linear Convolution on a given AudioBuffer, often used to achieve a reverb effect". The buffer you give it holds what's called an impulse response.
As I understand it, an impulse response is what a space gives back when you feed it one very short sound. Wikipedia says impulse responses are used to capture "the reverb at a location", and that packs of them "from specific locations are available online". MDN's own example loads one from a file.
Convolution, in plain words: every tiny moment of your sound is replaced by a quiet copy of the whole impulse response. Feed in a click and out comes the room's tail. Feed in a note and out comes the note smeared through that tail.
The trick
Listen to a room's tail on its own and it is close to a burst of hiss that fades away. So I make that directly: random numbers, multiplied by a curve that falls from full to nothing. Left and right get different random numbers, which makes the tail sound wide.
function makeImpulse(ctx, seconds, decay) {
const length = Math.floor(ctx.sampleRate * seconds);
const impulse = ctx.createBuffer(2, length, ctx.sampleRate);
for (let ch = 0; ch < 2; ch++) {
const data = impulse.getChannelData(ch);
for (let i = 0; i < length; i++) {
const fade = Math.pow(1 - i / length, decay);
data[i] = (Math.random() * 2 - 1) * fade;
}
}
return impulse;
}
const reverb = ctx.createConvolver();
reverb.buffer = makeImpulse(ctx, 3, 2);
The numbers in the last line are placeholders, not my settings. There are two knobs. seconds is how long the tail lasts, which you hear as the size of the room. decay is how steeply it falls away: a bigger number drops faster at the start, and the room sounds smaller and drier.
What I changed after listening
- I softened the very start. The plain version begins at full loudness on its first sample, and that put a faint hard edge on every sound. Mine rises for a brief moment before it falls.
- I took the top off. Noise is hissy, so the tail was hissy. A low-pass filter after the reverb keeps it soft.
- One room for everything. All the sounds pass through a single point on their way out, and the reverb hangs off that point. Every sound lands in the same space, and I never had to wire each one up separately.
- A different room for each world. Forest has the longest tail, like a big stone hall. Space has the shortest, and gets its sense of distance from a long, dark echo instead. A new room is generated on the spot when you switch worlds.
What you give up
This room is no real place. A recorded hall has its own character, which comes from its particular walls. Mine is a smooth, anonymous wash.
For background sound that suited me. You shouldn't notice the room at all. If you need one particular cathedral, you need a recording of it.
One small side effect: the noise is random, so the room is slightly different in its fine detail every time the page loads. I can't hear the difference.
Sources
Try it: Open Ambient Sound Online and bring two shapes together in Ocean. It's free. The soft tail that hangs on after each sound is the made-up room.
Open Ocean, free