WebAudio Deep Note, part 5: gain node

November 9th, 2019. Tagged: JavaScript, WebAudio

Previously on "Deep Note via WebAudio":

  1. intro
  2. play a sound
  3. loop and change pitch
  4. multiple sounds
  5. nodes

In part 4 we figured out how to play all 30 sounds of the Deep Note at the same time. The problem is that's way too loud. Depending on the browser and speakers you use you may: go deaf (usually with headphones), get distortion (Chrome), your OS turns it down (Mac, built-in speakers) or experience any other undesired effect. We need to "TURN IT DOWN!". This is where the Gain node comes in. Think of it as simply volume.

Plug in the Gain node

So we have this sort of node graph:

all the notes

And we want to make it like so:

all the notes with gain

Having this will allow us to turn down the volume of all the sounds at the same time.

The implementation is fairly straightforward. First, create (construct) the node:

const volume = audioContext.createGain();

Its initial value is 1. So turn it way down:

volume.gain.value = 0.1;

Connect (plug in) to the destination:

volume.connect(audioContext.destination);

Finally, for every sound, instead of connecting to the destination as before, connect to the gain node:

// BEFORE:
// source.connect(audioContext.destination);
// AFTER:
source.connect(volume);

Ahhh, that's much easier on the ears.

AudioParam

As you see, the Gain node we called volume has a gain property. This property is itself an object of the AudioParam type. One way to manipulate the audio parameter is via its value property. But that's not the only way. There are a number of methods too that allow you to manipulate the value in time, allowing you to schedule its changes. We'll do just that in a second.

Personal preference

I like to call my gain nodes "volume" instead of "gain". Otherwise it feels a little parrot-y to type gain.gain.value = 1. Often I find myself skipping one of the gains (because it feels awkward) and then wondering why the volume isn't working.

Gain values

0 is silence, 1 is the default. Usually you think of 1 as maximum volume, but in fact you can go over 1, all the way to infinity. Negative values are accepted too, they work just like the positive ones: -1 is as loud as 1.

Scheduling changes

Now we come to the beginning of the enchanting journey through the world of scheduling noises. Let's start simple. Deep Note starts out of nothing (a.k.a. silence, a.k.a. gain 0) and progresses gradually to full volume. Let's say it reaches full volume in 1 second.

Thanks to a couple of methods that every AudioParam has, called setValueAtTime() and setTargetAtTime(), we can do this:

volume.gain.setValueAtTime(0, audioContext.currentTime);
volume.gain.setTargetAtTime(0.1, audioContext.currentTime, 1);

And we do this whenever we decide to hit the Play button. The first line says: right now, set the volume (the gain value) to 0. The second line schedules the volume to be 0.1. audioContext.currentTime is the time passed since the audio context was initialized, in seconds. The number 1 (third argument in the second line) means that it will take 1 second to start from 0, move exponentially and reach the 0.1 value. So in essence we set the gain to 0 immediately and also immediately we begin an exponential transition to the value 0.1 and get there after a second.

All in all there are 5 methods that allow you to schedule AudioParam changes:

  • setValueAtTime(value, time) - no transitions, at a given time, set the value to value
  • setTargetAtTime(value, start, duration) - at start time start moving exponentially to value and arrive there at start + duration o'clock
  • exponentialRampToValueAtTime(value, end) - start moving exponentially to value right now and get there at the end time
  • linearRampToValueAtTime() - same as above, but move lineary, not exponentially
  • setValueCurveAtTime(values, start, duration) - move through predefined list of values

Above we used two of these functions, let's try another one.

A gentler stop()

Sometimes in audio you hear "clicks and pops" (see the "A note on looping a source" in a previous post) when you suddenly cut off the waveform. It happens when you stop a sound for example. But we can fix this, armed with the scheduling APIs we now know of.

Instead of stopping abruptly, we can quickly lower the volume, so it's imperceptible and sounds like a stop. Then we stop for real. Here's how:

const releaseTime = 0.1;

function stop() {
  volume.gain.linearRampToValueAtTime(
    0, 
    audioContext.currentTime + releaseTime
  );
  for (let i = 0; i < sources.length; i++) {
    sources[i] && sources[i].stop(audioContext.currentTime + 1);
    delete sources[i];
  }
}

Here we use linearRampToValueAtTime() and start turning down the volume immediately and reach 0 volume after 0.1 seconds. And when we loop through the sources, we stop them after a whole second. At this time they are all silent so that time value doesn't matter much. So long as we don't stop immediately.

That's a neat trick. Every time you suffer pops and clicks, try to quickly lower the volume and see if that helps.

And what's the deal with all the exponential stuff as opposed to linear? I think we perceive exponential changes in sound as more natural. In the case above it didn't matter since the change is so quick it's perceived as an immediate stop anyway.

Bye-o!

A demo of all we talked about in this post is here, just view source for the complete code listing.

Thanks for reading and talk soon!

Tell your friends about this post on Facebook and Twitter

Sorry, comments disabled and hidden due to excessive spam.

Meanwhile, hit me up on twitter @stoyanstefanov