Synthesizing Musical Sounds with Web Audio API – Part 1

Web Audio API is a system for creating, controlling, and manipulating audio in a Web browser using JavaScript.

This series of articles is intended to demonstrate the basic concepts needed to synthesize musical sounds using Web Audio API.

Let’s start out by simply starting and stopping the sounding of a tone.

If you open this in a browser and click the “On” radio button, you should hear a tone, and the tone should stop when you click the “Off” radio button. Although there is nothing in this code to say what timbre (waveform, kind) of sound to make or at what pitch, the sound we hear is a sine wave at 440 Hz (Middle A). That is because of the default values of the type and frequency properties for an OscillatorNode which are “sine” and 440 respectively.

If you start and stop the tone a few times, you will probably notice an unpleasant click/pop sound sometimes when you turn the tone off. Also, although there is not a click or pop at the start of the tone, it does start very abruptly. We’ll deal with those issues, but first, let’s dig into what this code is doing so far.

Before the first time the tone is sounded, we have to create an AudioContext object. Everything else that we do is in relation to this context. Using that, we then create an OscillatorNode that will generate the sound that we’re going to hear. The browser routes audio signals to the computer’s speakers through the destination property of the audio context, so to allow the oscillator’s output to be heard, we connect it to that.

Finally, we call the start method of the oscillator so that it starts producing a tone.

When the tone is playing, we can then stop it by simply calling the stop method of the oscillator.

Notice that we clear the reference to the oscillator after we stop the tone, and we create a new oscillator to play the tone again to re-start it. You may ask why don’t we just keep one oscillator around and start it again. That is because an OscillatorNode object is actually intended to be used one time and then discarded and can only be started and stopped once. It cannot be used to make sound again after its stop method has been called.

Now, getting back to that click when stopping the oscillator. That happens because the oscillator has been producing a smoothly changing sine wave as output, and we stop it at an arbitrary moment in the middle of a wave cycle. Similarly, even though the start of the tone will happen at at beginning of a wave, it is an abrupt start, jumping immediately to full volume.

We can solve both of those problems by adding a short fade-in at when starting the tone and fade-out when stopping the tone.

Since an oscillator provides no way to control its volume directly, we route it through a GainNode.

We can control the amount of gain using the GainNode’s gain property. The gain property is not simply a value though. It refers to an AudioParam object. The AudioParam object has a value property, but if any of the time-based methods are being used with an AudioParam (as we are doing here) then it’s usually best to use only those methods and not the value property.

The context has a currentTime value which is a floating point number of seconds since the context was started, and all the time values we pass to methods of objects associated with that context are with respect to that. Since the JavaScript code itself takes a non-zero amount time to run, we generally want to schedule everything at least a tiny bit into the future by adding a fraction of a second to the current time. Note that we can schedule parameter changes in the past (including at time 0) and the result of that is that the current value jumps to what it would be now if that had actually happened as scheduled.

For starting the tone, we set the gain to 0.0 at time 0, and then we also set the gain to 0.0 at a time slightly into the future. That becomes the starting time and value for the subsequent ramp up. We then ramp the value up to 1.0 ending at a time slightly farther into the future. That gives us a gentle rise to maximum volume so the start is not so abrupt.

For ending the tone, we essentially do the same thing in reverse. The gain was ramped up to 1.0 when the tone was started, so we set it to 1.0 again at a point slightly in the future to be the starting point for ramping down to 0.0. We then ramp the value down to 0.0 ending at a point slightly farther into the future. We don’t want to stop the oscillator until its gain has been ramped all the way down, so we schedule the stop to happen at the same time as the end of the ramp-down. This eliminates that nasty click that we previously could have when stopping the tone.

The oscillator has an onended event handler property, and we use that to schedule the cleanup of the oscillator-related references after we’re completely done with them.

You might have noticed that there’s a bigger delay before starting the ramp-down for stopping the tone than there is before starting the ramp-up for starting it. This is to make sure that if the tone is started and stopped quickly, it has had time to ramp all the way up before we set it to 1.0 to start the ramp-down. There is possibly a better way to handle that in the form of AudioParam.cancelAndHoldAtTime(), but as of this writing, that is still “experimental technology”.

In part 2 of this series, we will explore how to control the pitch and timbre of the tone.

References: