Making Music with The WebAudio API Part 4

At the end of part 3 of this series, we had an HTML document for a page with buttons to start and stop playing a tone, we could select a musical note pitch for the tone, and we could change the volume. There were also some things about it that were a little rough that we’d now like to improve.

Here’s what the HTML for our page looks like so far.

The next issue we’ll address is the fairly obvious click that we hear when stopping a tone or changing the volume while a tone is playing. This problem is caused by abruptly stopping playback or changing the gain at an arbitrary point in the waveform. The way to solve it is to make gain changes gradually rather than suddenly.

Let’s solve the problem with volume changes first because it’s the simpler case.

Our gain variable contains a GainNode, and the .gain property of a GainNode contains an AudioParam. Besides having a .value property that we can get or set, an AudioParam also has many other methods that we can utilize. The most suitable of those for our purpose is .linearRampToValueAtTime().

Note that we are not using .exponentialRampToValueAtTime() because, although that might sound more musical, the linear ramp is perfectly adequate here, and exponential cannot be used for ramping to a value of 0 which we want to be able to do.

In order for the first call to .linearRampToValueAtTime() to work properly, we must have previously set the audio parameter’s value using one of the scheduling methods, so the next thing to do is add the following right after the gain.connect( audioCtx.destination ); line.

Now we can implement ramp the volume changes by changing the changeGain() function to read as follows.

Note that the endTime parameter must contain a time relative to when our audio context was first started. That is why we add the 0.1 seconds ramp duration to audioCtx.currentTime.

Having addressed the clicks when changing volume, now let’s take care of the clicks when stopping notes. To do that, we’ll change the stopTone() function to read as follows.

Notice that we first set the gain to 1 at a specific time and then ramp to 0 ending at a time 1/20 of a second later. The ramp will start right after the previously scheduled value change is done, so the total ramp time will be 1/20 of a second. So why the initial delay of 1/20 of a second before setting the value to 1? Frankly, I don’t know why this is necessary, but I have found that if that delay is not given, then we don’t get the desired result and still hear a click when stopping the note. If you happen to know why, please share that in a comment on this article.

Finally, another 1/20 of a second after the end of the ramp, we stop the oscillator. We also move our button state toggling into the handler for an event that gets called after the oscillator has been stopped. That way, we don’t let the user try to start a new tone while we’re still in the process of stopping the previous tone.

The next article in this series will cover how to specify the volume/gain in terms of Decibels (exponential) rather than linear ratio units.