Pretty simple! Note that when you set up an oscillator, you specify when in the future to start (in the case above, we’re asking the audio context to start right now). That means you can schedule lots of future notes at once. Let’s extend the example above to play a few more frequencies.
These examples are going to get really long if we keep repeating ourselves like this, so let’s make a function to encapsulate playing a note.
Hey, that actually sounds like music!
Before we continue, let’s talk about that clicking. Depending on your current browser, the example above may have distracting clicks at the either the front or back of each note (or both). Our oscillator, which generates a sound at a frequency, is abruptly switching on and switching off. In real life, sound doesn’t work that way - each sound you hear has a certain “envelope”, which means it fades in from zero, lasts for a certain duration, and then fades back out to zero at the end.
We can create an envelope for our notes by managing gain (volume). To do that, we need to create an audio gain node, and connect our oscillator notes to the gain node. Then we will add some additional logic to our play function, which will micro-manage the volume over time:
Try alternating between the previous demo and this one to see the difference. Note that the feel of this envelope can be modified by playing with the values I chose above. Check out the examples below to see how you can get quite different sounds, not by modifying frequency or length in any way, but just by playing with the sound’s envelope:
The frequency of a note is measured in Hz, or cycles per second. Exactly how pitch and frequency work, and musical theories behind what notes sound good together, all of that is out of scope for this tutorial; let’s just assume you have some idea what you’d like to play - maybe a simple melody like “C, E, G, F”.
Luckily, there’s a pretty easy way to play just notes - we can use the A440 pitch, which is the musical note of A above middle C on a standard grand piano, and detune it. That means that instead of attempting to calculate frequencies directly, we can provide a detune value from A440, like this:
A detune value is provided in cents, or hundredths of a pitch. There are twelve pitches on the chromatic
scale, which is the scale you see on a piano -
G sharp, and then looping back to A again. Knowing this, you can construct a simple detune table
for the notes you’d like to play:
If you’d like to keep going up or down, just add or subtract 1200 to move up or down an octave.
Let’s use this new information to update our
play function, and play the first few notes of “Mary Had A Little Lamb”:
Math Tip: In case you need it, it is pretty easy to calculate frequencies based on cents yourself. Note that
cents represent a difference between two frequencies. Given an initial frequency
f1, and a cents value
you can compute the modified frequency
f2 = f1 * Math.pow(2, c / 100).
Sometimes it’s nice to play some overlapping notes. That would be difficult with our current code, because we’re modifying the volume to represent our sound’s envelope; we can’t have two different notes trying to control the volume at the same time.
In order to fix this, you can create multiple gain nodes, and rotate between them, ensuring that only one note is controlling the volume of that node at a time. Let’s try it by making our notes above overlap:
In the real world, you typically don’t want to schedule an entire song’s worth of notes ahead of time. You want to be able to start and stop, you probably want to be able to loop around to the beginning if you reach the end, etc.
To accomplish this, you can have a function that you call periodically (perhaps on a timer, or in a game,
during your frame handling in your
requestAnimationFrame callback). Here’s an example of what that might
Make sure to test how your game or application responds to minimizing the browser and changing tabs.
As a general rule, the audio context will continue to play any scheduled notes, but all animation
frames will pause. This will mean that if you rely on animation frames to play your music, it’ll
stop playing when the user switches tabs. More importantly, it means that when you come back to
your tab, your
nextNoteTick value may be minutes behind
ctx.currentTime. The best thing to do
in this case is to check for it in your function, and fast-forward
nextNoteTick to the
current time if it is more than a second or two behind.
This guide only scratches the surface of what’s possible with the Web Audio API. I recommend jumping into a more general tutorial, like Getting Started with Web Audio API, to get a larger-scale overview.
Another option is to use a pre-existing music library created especially for small games - check out the js13k Resources page for some ideas.