Loading Symbol

JS Synthesizer Part 5: Gain Staging and Dynamics Processing

AudioNode Flow Diagram

In part five we’ll restructure the gain staging in our synthesizer to prevent clipping using the Web Audio API’s DynamicsCompressorNode and GainNode . This will touch on some of the basics of dynamics processing. In the last post, we gave the user the ability to play multiple notes at once with the computer keyboard. However, if they do this, they will likely here distortion – multiple notes will overload the main bus. We want to prevent that with a limiter.

This tutorial is part five in a series that will show you how to create a virtual synthesizer using JavaScript and the Web Audio API. Click here to clone or download the working code examples from the GitHub repository. Click here to view the finished product that uses the concepts and code discussed in this series. To get a better idea of the complete process, checkout the overview.

Why Add Dynamics Processing?

There are many cases where you’d want to add dynamics processing. In audio engineering, you use a compressor to reduce the dynamic range of an audio source. Sometimes it’s applied as a way to color the sound, but more importantly, it can be used to prevent overloading buses. That’s how we’re using it here. Listen to this recording I made of our synthesizer playing multiple notes.

Clipping

That distortion is a result of the master bus being overloaded. Now listen to how it sounds after we put a limiter on the master bus.

No Clipping

How to Get Rid of Clipping/Distortion

We git rid of the clipping by adding a limiter to the master bus. The master bus in this case is the GainNode through which all our voices will be routed – the master gain node.

First we’re going to create the AudioContext as we did before. Immediately after this, we’ll create a pregain node (GainNode), limiter node (DynamicsCompressorNode), and master gain node (GainNode).

// Create AudioContext
var audioCtx = new(window.AudioContext || window.webkitAudioContext)();
var pregainNode = audioCtx.createGain();
var masterGainNode = audioCtx.createGain();
masterGainNode.gain.setValueAtTime(-0.3, audioCtx.currentTime);  // Amount of gain in db -infinity to +infinity. -0.3 leaves a little headroom.

var limiterNode = audioCtx.createDynamicsCompressor();
// Creating a compressor but setting a high threshold and 
// high ratio so it acts as a limiter. More explanation at 
// https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode
limiterNode.threshold.setValueAtTime(-5.0, audioCtx.currentTime); // In Decibels
limiterNode.knee.setValueAtTime(0, audioCtx.currentTime); // In Decibels
limiterNode.ratio.setValueAtTime(40.0, audioCtx.currentTime);  // In Decibels
limiterNode.attack.setValueAtTime(0.001, audioCtx.currentTime); // Time is seconds
limiterNode.release.setValueAtTime(0.1, audioCtx.currentTime); // Time is seconds

pregainNode.connect(limiterNode);
limiterNode.connect(masterGainNode);
masterGainNode.connect(audioCtx.destination);

You can read more about the DynamicsCompressorNode settings here. These settings are creating a brickwall limiter which is what we want.

Next we’re going to update the Voice.start() method to connect the oscillator output to the pregainNode instead of connecting it directly to the AudioContext’s destination.

this.oscillator.connect(pregainNode);

The pregain node is not really being used at this time, but this flow is a good starting place for future enhancements.

The pregain node is not really necessary right now, but this flow is a good starting place for future enhancements.

AudioNode Flow Diagram

That’s it! You should be able to hold multiple keys at once and still have a nice undistorted sound. You will still notice a pop when you start and stop notes – especially when using a sine wave. This is caused by the immediate jump from – ∞dB to -1dB (or some audible level). The next post in this series is about envelopes, and it will show you how to solve that problem.


Loading Symbol


Leave a Reply

Your email address will not be published. Required fields are marked *