Previously on "Deep Note via WebAudio":
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:
And we want to make it like so:
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 giventime
, set the value tovalue
setTargetAtTime(value, start, duration)
- atstart
time start moving exponentially tovalue
and arrive there atstart + duration
o'clockexponentialRampToValueAtTime(value, end)
- start moving exponentially tovalue
right now and get there at theend
timelinearRampToValueAtTime()
- same as above, but move lineary, not exponentiallysetValueCurveAtTime(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!
Comments? Feedback? Find me on Twitter, Mastodon, Bluesky, LinkedIn, Threads