Modeling an analog delay in the Web Audio API
The Web Audio API gives developers access to a powerful set of tools to develop audio applications on the internet. In this article, we’ll look at a way to take inspiration from the analog world while leveraging the flexibility of the digital domain.
In order to understand how to use the Web Audio API, it helps to visualize signal flow in the way that an audio engineer would. With that in mind, let’s take a look at the architecture of what we’re building.
The signal enters at the input, and if you follow the right side of the diagram, it hits a gain node and then continues to the output. This is our “dry” signal. The “wet” signal first hits a low pass filter, which removes high frequencies from the sound. This is important for capturing the timbre and behavior of an analog delay. From there it goes through a delay node, which is the core of our effect. Then the signal splits. Half will go to the feedback portion of the circuit, where it will circle back through the filter and delay multiple times, depending on how high the feedback gain is set. This is how we create multiple repeats.
Optionally, we can apply an LFO or “low frequency oscillator” to the delay node to slightly vary the delay time. Modulating the delay time slightly will alter the pitch of the delayed sound, which creates a “chorus” effect as it blends with the original sound. Finally, the signal continues from the delay node through to the “wet” gain before proceeding to the output, where it blends with the “dry” signal.
Let’s dive into the code. First we create our audio context. The following function will help with cross-browser implementations of the Web Audio API. Safari and iOS still use webkitAudioContext as of now, which in some use-cases will require special attention. But for this article, the code below will suffice.
const newContext = () => {
const audioContext = window.AudioContext || window.webkitAudioContext;
return new audioContext();
}const context = newContext();
Next we’ll instantiate our Delay class, passing into the constructor a reference to the audio context, the delay time in seconds, and the amount of feedback, which defaults to 0.3. A gain node with a value of 1 will not alter the volume of the signal, resulting in an infinite loop. Therefore our default setting will yield a small number of repeats that fade away, as the amplitude of the signal decreases by 70% each time around the feedback loop.
class Delay {
constructor(context, time, feedback = 0.3) {
// ...
}
}
I like to give each effect I build an input gain and an output gain. This helps with gain staging, but it also makes it very easy to connect everything together. This way, you can always say:
someEffect.output.connect(someOtherEffect.input);
To implement this, we will build out our constructor. Let’s add the delay node while we’re at it.
this.input = context.createGain();
this.output = context.createGain();
this.delay = context.createDelay();
this.delay.delayTime.value = time;
According to our block diagram, we need 3 more gain nodes — one for dry gain, one for wet gain, and one for feedback.
this.dry = context.createGain();
this.wet = context.createGain();
this.feedback = context.createGain();
this.feedback.gain.value = feedback;
Now we’re ready to connect the “dry” side of our signal path:
this.input.connect(this.dry);
this.dry.connect(this.output);
Only one piece missing from our feedback loop — a lowpass filter. With that created, we can wire up the “wet” signal. A good default setting for the filter is 2000 Hz. This will take away the high frequencies from the delayed signal, which is what a real analog delay will do.
this.filter = context.createBiquadFilter();
this.filter.type = "lowpass";
this.filter.frequency.value = 2000; // Freq. in Hz // feedback loop
this.input.connect(this.filter);
this.filter.connect(this.delay);
this.delay.connect(this.feedback);
this.feedback.connect(this.filter); // connect to output
this.delay.connect(this.wet);
this.wet.connect(this.output);
If we stopped here, we would have a fully functioning delay effect. The icing on the cake is an LFO that modulates the delay time to produce a bit of “chorus” effect. We will use the default “sine” wave as our waveform, and assign a frequency of 0.5 Hz (1 cycle every 2 seconds). Then we route it through a gain node so that the target parameter (delay time) is modulated on the right order of magnitude. I like a very small amount of modulation for this to sound “correct” to my ear, but you can play around with the LFO frequency and gain values and see what happens.
// create LFO and set values
this.lfo = context.createOscillator();
this.lfo.frequency.value = 0.5; // Freq. in Hz
this.lfoGain = context.createGain();
this.lfoGain.gain.value = 0.0005;
this.lfo.start(); // start the oscillator// connections
this.lfo.connect(this.lfoGain);
this.lfoGain.connect(this.delay.delayTime); // modulates a parameter
And there you have it! We have just created a fully functional model of an analog delay pedal in the Web Audio API. Parameters to experiment with would be the wet and dry gain levels, delay time, feedback gain, LFO frequency, and LFO gain. All that’s left to do is connect an input source, build a UI, and let your creativity run wild. We cover how to do this in Part 2.
Did you like this article? Want more content like this? Follow me on Twitter and let me know!