Skip to content

Instantly share code, notes, and snippets.

@maxymania
Last active August 2, 2025 06:57
Show Gist options
  • Save maxymania/7cce289a2e720b5761e6b97b674db27f to your computer and use it in GitHub Desktop.
Save maxymania/7cce289a2e720b5761e6b97b674db27f to your computer and use it in GitHub Desktop.
Web Audio API based Impulse Response Generator for ConvolverNode based Reverb

Web Audio API based Impulse Response Generator

This is an example source code, enables the use of the ConvolverNode of the Web Audio API. The convolution based reverb is a really powerful tool, but one might not know where to get started. Typically, there is an elaborate process of recording an impulse response with real equipment in a real room, or using a program to capture a certain filter as described in this video, but if you come from OpenAL, or if you want to start from scratch real quick, you'll need a simpler solution.

I have experimented with the ConvolverNode and initially, I created an AudioBuffer and filled it with white noise, and supprisingly, it worked. When I faded the white noise to silence, it worked even better. So, I used an OfflineAudioContext to generate the frequency response using the parameters of OpenAL's <AL/efx-preset.h> header, which to me is the holy grail of environmental audio effects. And I was pleased with the result.

Have fun.

Explanation

The easiest way to get a simple reverb without a pre-recorded, pre-generated or bought impulse-response file is to create an AudioBuffer and fill it with white noise that starts strong and fades out.

let buf = myCtx.createBuffer(1,48000,48000);
let f = buf.getChannelData(0)
f[0] = 1;
for(let i=1;i<f.length;++i)
  f[i] = rnd() * Math.pow((f.length-i)/f.length,2);
let myReverb = new ConvolverNode(myCtx, {buffer:buf, channelCount:2});

While effective, this reverb is often not enough. If you want to model realistic soundshapes of an environment such as a castle, mansion or chapel, it is desireable to add a certain amount of coloration. This can be done by colorizing the noise. In the image below, you can see 4 lanes of noise. The first 3 lanes contain, in the following order: 1. Noise above flHFReference Hz. 2. Noise between flLFReference and flHFReference Hz. and 3. Noise below flLFReference Hz. Adding them all up would result in White Noise.

.   [Noise]   [Noise]   [Noise]   [Noise]
.      |         |         |         |
.    [HPF]     [LPF]     [LPF]       |
.      |         |         |         |
.      |       [HPF]       |         |
.      |         |         |         |
.    [Att]     [Att]     [Att]     [Att]
.      |         |         |         |
.       \        |         |        /
.        \       |         |       /
.         \      |         |      /
.          \      \       /      /
.           \      \     /      /
.         ------------------------ 
.         |  Combine All Inputs  |
.         ------------------------
.                     |
.                 [Result]

Both Noise and Att are implemented using the AudioWorklet of the OfflineAudioContext. The Att nodes in the diagram are 'multiplier-node' worklet nodes. These have 2 inputs, and they multiply both inputs sample by sample. The first input is the noise, that is to be attenuated/faded and the second input is the amplitude on a per-sample granularity. In other words, the multiplier node implements a tightly controlled gain, programmed on a per-sample base. They are programmed using AudioBuffers containing a fade-out curve multiplied with the gain, specified by the preset input.

let d0 = ctx.createBuffer(1,480,48000);
let d1 = ctx.createBuffer(1,decaySampsHf,48000);
let d2 = ctx.createBuffer(1,decaySamps,48000);
let d3 = ctx.createBuffer(1,decaySampsLf,48000);
for(let x of [d0,d1,d2,d3])
{
  let samps = x.getChannelData(0);
  for(let i=0,n=samps.length;i<n;++i)
    samps[i] = Math.pow((n-i)/n,2);
}
let gains = [
  [d1, pso.flGain * pso.flGainHF],
  [d2, pso.flGain],
  [d3, pso.flGain * pso.flGainLF],
];
for(let pair of gains)
{
  let [x,gain] = pair;
  let samps = x.getChannelData(0);
  for(let i=0,n=samps.length;i<n;++i)
    samps[i] *= gain;
}

Lets speak of the 4th lane in the diagram above: It is white noise, that is faded using a very short fadeout curve. It has a duration of 10ms. This is put in place, so that original sound is displayed as well and not only the reverb/reflection of it.

/*
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>
--------------------------------------------------------------------------
This file contains some stuff, that is copy-pasted from a C-header belonging
to OpenAL, namely "AL/efx-presets.h". The header itself doesn't have a
Copyright notice. If you're unsure, remove everything down to BakePreset().
*/
function EfxArrayToObject(arr)
{
let [
flDensity,
flDiffusion,
flGain,
flGainHF,
flGainLF,
flDecayTime,
flDecayHFRatio,
flDecayLFRatio,
flReflectionsGain,
flReflectionsDelay,
flReflectionsPan,
flLateReverbGain,
flLateReverbDelay,
flLateReverbPan,
flEchoTime,
flEchoDepth,
flModulationTime,
flModulationDepth,
flAirAbsorptionGainHF,
flHFReference,
flLFReference,
flRoomRolloffFactor,
iDecayHFLimit
] = arr;
return {
flDensity,
flDiffusion,
flGain,
flGainHF,
flGainLF,
flDecayTime,
flDecayHFRatio,
flDecayLFRatio,
flReflectionsGain,
flReflectionsDelay,
flReflectionsPan,
flLateReverbGain,
flLateReverbDelay,
flLateReverbPan,
flEchoTime,
flEchoDepth,
flModulationTime,
flModulationDepth,
flAirAbsorptionGainHF,
flHFReference,
flLFReference,
flRoomRolloffFactor,
iDecayHFLimit
};
}
EfxPresets = {
GENERIC : EfxArrayToObject([ 1.0000 , 1.0000 , 0.3162 , 0.8913 , 1.0000 , 1.4900 , 0.8300 , 1.0000 , 0.0500 , 0.0070 , [ 0.0000 , 0.0000 , 0.0000 ], 1.2589 , 0.0110 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
ROOM : EfxArrayToObject([ 0.4287 , 1.0000 , 0.3162 , 0.5929 , 1.0000 , 0.4000 , 0.8300 , 1.0000 , 0.1503 , 0.0020 , [ 0.0000 , 0.0000 , 0.0000 ], 1.0629 , 0.0030 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
BATH : EfxArrayToObject([ 0.1715 , 1.0000 , 0.3162 , 0.2512 , 1.0000 , 1.4900 , 0.5400 , 1.0000 , 0.6531 , 0.0070 , [ 0.0000 , 0.0000 , 0.0000 ], 3.2734 , 0.0110 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
LIVING : EfxArrayToObject([ 0.9766 , 1.0000 , 0.3162 , 0.0010 , 1.0000 , 0.5000 , 0.1000 , 1.0000 , 0.2051 , 0.0030 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2805 , 0.0040 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
STONE : EfxArrayToObject([ 1.0000 , 1.0000 , 0.3162 , 0.7079 , 1.0000 , 2.3100 , 0.6400 , 1.0000 , 0.4411 , 0.0120 , [ 0.0000 , 0.0000 , 0.0000 ], 1.1003 , 0.0170 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
AUDITORIUM : EfxArrayToObject([ 1.0000 , 1.0000 , 0.3162 , 0.5781 , 1.0000 , 4.3200 , 0.5900 , 1.0000 , 0.4032 , 0.0200 , [ 0.0000 , 0.0000 , 0.0000 ], 0.7170 , 0.0300 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
CONCERT : EfxArrayToObject([ 1.0000 , 1.0000 , 0.3162 , 0.5623 , 1.0000 , 3.9200 , 0.7000 , 1.0000 , 0.2427 , 0.0200 , [ 0.0000 , 0.0000 , 0.0000 ], 0.9977 , 0.0290 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
CARPET : EfxArrayToObject([ 0.4287 , 1.0000 , 0.3162 , 0.0100 , 1.0000 , 0.3000 , 0.1000 , 1.0000 , 0.1215 , 0.0020 , [ 0.0000 , 0.0000 , 0.0000 ], 0.1531 , 0.0300 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
HALLWAY : EfxArrayToObject([ 0.3645 , 1.0000 , 0.3162 , 0.7079 , 1.0000 , 1.4900 , 0.5900 , 1.0000 , 0.2458 , 0.0070 , [ 0.0000 , 0.0000 , 0.0000 ], 1.6615 , 0.0110 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
STONEHALLWAY : EfxArrayToObject([ 1.0000 , 1.0000 , 0.3162 , 0.7612 , 1.0000 , 2.7000 , 0.7900 , 1.0000 , 0.2472 , 0.0130 , [ 0.0000 , 0.0000 , 0.0000 ], 1.5758 , 0.0200 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
CASTLE : EfxArrayToObject([ 1.0000 , 0.9300 , 0.3162 , 0.2818 , 0.1000 , 2.0400 , 0.8300 , 0.4600 , 0.6310 , 0.0220 , [ 0.0000 , 0.0000 , 0.0000 ], 1.5849 , 0.0110 , [ 0.0000 , 0.0000 , 0.0000 ], 0.1550 , 0.0300 , 0.2500 , 0.0000 , 0.9943 , 5168.6001 , 139.5000 , 0.0000 , 0x1 ]),
CASTLEHALL : EfxArrayToObject([ 1.0000 , 0.8100 , 0.3162 , 0.2818 , 0.1778 , 3.1400 , 0.7900 , 0.6200 , 0.1778 , 0.0560 , [ 0.0000 , 0.0000 , 0.0000 ], 1.1220 , 0.0240 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 5168.6001 , 139.5000 , 0.0000 , 0x1 ]),
FACTORY : EfxArrayToObject([ 0.4287 , 0.8200 , 0.2512 , 0.7943 , 0.5012 , 2.7600 , 0.6500 , 1.3100 , 0.2818 , 0.0220 , [ 0.0000 , 0.0000 , 0.0000 ], 1.4125 , 0.0230 , [ 0.0000 , 0.0000 , 0.0000 ], 0.1740 , 0.0700 , 0.2500 , 0.0000 , 0.9943 , 3762.6001 , 362.5000 , 0.0000 , 0x1 ]),
ICEPALACE : EfxArrayToObject([ 1.0000 , 0.8700 , 0.3162 , 0.5623 , 0.4467 , 2.2200 , 1.5300 , 0.3200 , 0.3981 , 0.0390 , [ 0.0000 , 0.0000 , 0.0000 ], 1.1220 , 0.0270 , [ 0.0000 , 0.0000 , 0.0000 ], 0.1860 , 0.1200 , 0.2500 , 0.0000 , 0.9943 , 12428.5000 , 99.6000 , 0.0000 , 0x1 ]),
WOODEN : EfxArrayToObject([ 1.0000 , 1.0000 , 0.3162 , 0.1000 , 0.2818 , 1.4700 , 0.4200 , 0.8200 , 0.8913 , 0.0490 , [ 0.0000 , 0.0000 , 0.0000 ], 0.8913 , 0.0290 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.0000 , 0.9943 , 4705.0000 , 99.6000 , 0.0000 , 0x1 ]),
MUSEUM : EfxArrayToObject([ 1.0000 , 0.8200 , 0.3162 , 0.1778 , 0.1778 , 3.2800 , 1.4000 , 0.5700 , 0.2512 , 0.0390 , [ 0.0000 , 0.0000 , -0.0000 ], 0.8913 , 0.0340 , [ 0.0000 , 0.0000 , 0.0000 ], 0.1300 , 0.1700 , 0.2500 , 0.0000 , 0.9943 , 2854.3999 , 107.5000 , 0.0000 , 0x0 ]),
LIBRARY : EfxArrayToObject([ 1.0000 , 0.8200 , 0.3162 , 0.2818 , 0.0891 , 2.7600 , 0.8900 , 0.4100 , 0.3548 , 0.0290 , [ 0.0000 , 0.0000 , -0.0000 ], 0.8913 , 0.0200 , [ 0.0000 , 0.0000 , 0.0000 ], 0.1300 , 0.1700 , 0.2500 , 0.0000 , 0.9943 , 2854.3999 , 107.5000 , 0.0000 , 0x0 ]),
DUSTYROOM : EfxArrayToObject([ 0.3645 , 0.5600 , 0.3162 , 0.7943 , 0.7079 , 1.7900 , 0.3800 , 0.2100 , 0.5012 , 0.0020 , [ 0.0000 , 0.0000 , 0.0000 ], 1.2589 , 0.0060 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2020 , 0.0500 , 0.2500 , 0.0000 , 0.9886 , 13046.0000 , 163.3000 , 0.0000 , 0x1 ]),
CHAPEL : EfxArrayToObject([ 1.0000 , 0.8400 , 0.3162 , 0.5623 , 1.0000 , 4.6200 , 0.6400 , 1.2300 , 0.4467 , 0.0320 , [ 0.0000 , 0.0000 , 0.0000 ], 0.7943 , 0.0490 , [ 0.0000 , 0.0000 , 0.0000 ], 0.2500 , 0.0000 , 0.2500 , 0.1100 , 0.9943 , 5000.0000 , 250.0000 , 0.0000 , 0x1 ]),
SMALLWATER :EfxArrayToObject([ 1.0000, 0.7000, 0.3162, 0.4477, 1.0000, 1.5100, 1.2500, 1.1400, 0.8913, 0.0200, [ 0.0000, 0.0000, 0.0000 ], 1.4125, 0.0300, [ 0.0000, 0.0000, 0.0000 ], 0.1790, 0.1500, 0.8950, 0.1900, 0.9920, 5000.0000, 250.0000, 0.0000, 0x0 ]),
};
// console.log(EfxPresets.GENERIC)
// ----------------------------------------------------------------------------------
// everything from here is essentially unencumbered.
async function BakePreset(pso)
{
let decaySamps = (48000*pso.flDecayTime)>>0;
let decaySampsHf = (48000*pso.flDecayTime*pso.flDecayHFRatio)>>0;
let decaySampsLf = (48000*pso.flDecayTime*pso.flDecayLFRatio)>>0;
let ctx = new OfflineAudioContext(1,decaySamps,48000);
await ctx.audioWorklet.addModule('./tools.js');
let n0 = new AudioWorkletNode(ctx,'noise-generator',{ numberOfInputs:0, outputChannelCount:[1]});
let n1 = new AudioWorkletNode(ctx,'noise-generator',{ numberOfInputs:0, outputChannelCount:[1]});
let n2 = new AudioWorkletNode(ctx,'noise-generator',{ numberOfInputs:0, outputChannelCount:[1]});
let n3 = new AudioWorkletNode(ctx,'noise-generator',{ numberOfInputs:0, outputChannelCount:[1]});
let f1 = new BiquadFilterNode(ctx,{type:'highpass', frequency: pso.flHFReference, channelCount: 1});
let f2a = new BiquadFilterNode(ctx,{type:'lowpass' , frequency: pso.flHFReference, channelCount: 1});
let f2b = new BiquadFilterNode(ctx,{type:'highpass', frequency: pso.flLFReference, channelCount: 1});
let f3 = new BiquadFilterNode(ctx,{type:'lowpass' , frequency: pso.flLFReference, channelCount: 1});
let m0 = new AudioWorkletNode(ctx,'multiplier-node',{ numberOfInputs:2, outputChannelCount:[1]});
let m1 = new AudioWorkletNode(ctx,'multiplier-node',{ numberOfInputs:2, outputChannelCount:[1]});
let m2 = new AudioWorkletNode(ctx,'multiplier-node',{ numberOfInputs:2, outputChannelCount:[1]});
let m3 = new AudioWorkletNode(ctx,'multiplier-node',{ numberOfInputs:2, outputChannelCount:[1]});
let d0 = ctx.createBuffer(1,480,48000);
let d1 = ctx.createBuffer(1,decaySampsHf,48000);
let d2 = ctx.createBuffer(1,decaySamps,48000);
let d3 = ctx.createBuffer(1,decaySampsLf,48000);
for(let x of [d0,d1,d2,d3])
{
let samps = x.getChannelData(0);
for(let i=0,n=samps.length;i<n;++i)
samps[i] = Math.pow((n-i)/n,2);
}
let gains = [
[d1, pso.flGain * pso.flGainHF],
[d2, pso.flGain],
[d3, pso.flGain * pso.flGainLF],
];
for(let pair of gains)
{
let [x,gain] = pair;
let samps = x.getChannelData(0);
for(let i=0,n=samps.length;i<n;++i)
samps[i] *= gain;
}
let p0 = new AudioBufferSourceNode(ctx,{buffer:d0});
let p1 = new AudioBufferSourceNode(ctx,{buffer:d1});
let p2 = new AudioBufferSourceNode(ctx,{buffer:d2});
let p3 = new AudioBufferSourceNode(ctx,{buffer:d3});
for(let p of [p0,p1,p2,p3]) p.start();
let o = ctx.createGain();
let g = ctx.createGain();
n0.connect(m0).connect(o);
n1.connect(f1).connect(m1).connect(o);
n2.connect(f2a).connect(f2b).connect(m2).connect(o);
n3.connect(f3).connect(m3).connect(o);
p0.connect(m0,0,1);
p1.connect(m1,0,1);
p2.connect(m2,0,1);
p3.connect(m3,0,1);
o.connect(ctx.destination);
let buf = await ctx.startRendering();
return buf;
}
/*
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>
*/
// This is to be run in the AudioWorklet
class NoiseGenerator extends AudioWorkletProcessor
{
constructor(opts)
{
super(opts);
let lops = opts.processorOptions || {};
this.replicate = lops.replicate || false;
}
process(inputs,outputs,whatever)
{
let {replicate} = this;
if(replicate)
{
let chan = outputs[0][0];
for(let i=0,n=chan.length;i<n;++i)
chan[i] = (Math.random()*2)-1;
for(let i=1,n=outputs[0].length;i<n;++i)
outputs[0][i].set(chan);
}
else
{
for(let chan of outputs[0])
for(let i=0,n=chan.length;i<n;++i)
chan[i] = (Math.random()*2)-1;
}
return true;
}
}
registerProcessor("noise-generator", NoiseGenerator);
class Multiplier extends AudioWorkletProcessor
{
constructor(opts)
{
super(opts);
let lops = opts.processorOptions;
}
process(inputs,outputs,whatever)
{
let s = inputs[0];
let m = inputs[1];
let d = outputs[0];
if(s.length==0) return true;
if(m.length==0) return true;
for(let i=0,n=d.length;i<n;++i)
{
let dc = d[i];
let mc = m[i]||m[0];
dc.set(s[i]||s[0]);
for(let j=0,m=dc.length;j<m;++j)
dc[j] *= mc[j];
}
return true;
}
}
registerProcessor("multiplier-node", Multiplier);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment