How to create music with Tone.js

Coding or music production? Why choose?

If you're anything like me, you love coding. You like the feeling you get when you solve a complex problem when you optimize a system or perhaps even solve that pesky little error that's been bugging you for the entire week. And yet programming is so much more than that. As programmers, we’re constantly learning new technologies and new principles, adapting as we go, and never stopping. I can, without a doubt, say that coding is a passion of mine as it is to so many of my peers.

Recently, I found that another activity creeping its way onto my passion list—music production. Like coding, I started studying producing music in-depth. I began thinking about how to split my time pursuing these two passions. Then it hit me. I don't have to choose.

Making music with Tone.js and JavaScript

Creating music has a lot more in common with programming than one might think. As I started learning more about music production and theory, I started seeing patterns in many places, such as how scales were created, how the most basic chords were made, etc. I immediately thought to myself, "Okay, programming could really come in handy here." Most of my background was in web development, so I looked around for something JavaScript-based that I could use. Then I found…drumroll please…Tone.js!

Tone.js is a nice, extensive framework aimed to create music and play it in a web browser. It's built on top of the Web Audio API as an abstraction layer, which makes for a perfect tool to create music using JavaScript. It's fairly robust and simple to use, which is great for beginners learning to program and produce music. That being said, it also allows for heavy customization in many ways (which I’ll get into later on in the article). In this blog, I’ll go over the basics of how to make music using Tone.js and JavaScript. Consider this a guide, we’re in this journey together, you and me.

Getting set up

There are a couple of ways to set up the project framework, such as using npm or node package manager. For the sake of simplicity and concentrating on the framework, here’s how you can create a single HTML file with the following content:

<script src="https://unpkg.com/tone"></script>
<button id="play-button">Play/Pause</button>
<script src="music.js"></script>

The first line imports the Tone.js framework from the web. Let's demo the button element later on to trigger sound, and start with the music.js the file.

First, let's make a little "hello world" type app to see if the framework works. For now, just add the following code to the music.js file. Don't worry. I'll show you what it does in just a bit.

const synth = new Tone.Synth().toMaster()
synth.triggerAttackRelease('C4', '8n')

document.getElementById("play-button").addEventListener("click", function() {
  if (Tone.Transport.state !== 'started') {
    Tone.Transport.start();
  } else {
    Tone.Transport.stop();
  }
});

Next, open up the HTML in a browser and click the Play/Pause button. Wow, it's just like magic! The C note on the 4th octave plays. How does this happen? Let's look line by line.

At the very top of the file, you’re creating an instance of a Synth object and then routing it to the "Master output." A Synth in Tone.js is a simple synthesizer with a single oscillator. If you’re not much of a music nerd, you may be wondering, "What the devil is an oscillator?!" Sn oscillator is a device which generates a tone by using a specific shape of a sound wave. You can read more on oscillators here if you're curious.

Routing the oscillator to the master output connects the synth to the main sound output of the computer. In the next line, call the triggerAttackRelease with a couple of arguments. This method triggers the sound the provided note (in this demo, it's C on the 4th octave) ends the sound after a set amount of time and provides the 8th note or '8n'.

The method’s name uses some lingo from sound design as well. The attack is the portion of the sound when the tone is just building up, while the release is the part where a tone fades away.

For this demo, all you need to know is that a note is a set amount of time to play a sound based on the current tempo of the track. 8th notes are twice as quick as quarter notes, which are twice as quick as half notes and so on. If you want to know more about time signatures and note types, you can read about that in great detail here.

Going back into the code, the next few lines use some basic JavaScript to add an event listener to the button. When triggered, either start or stop the Transport based on the current state. "What is a Transport?" you may ask. Think of Transport as a track; it's something that keeps the timing of musical events. You may have noticed that you didn't initialize the Transport instance anywhere. That is because a single instance of it is always created when the framework is initialized.

Creating scales and chords

Now that you know the basic concepts, how about creating something a little more complex?

Let's create a musical scale using A minor. A scale is simply a collection of notes that sound good together in a sequence. Notice the word sequence. Here is where you can put programming knowledge to use.

An A minor scale looks like this:

const AMinorScale = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];

Cool, nice, and simple. How about including a function that adds the octave numbers to the notes based on the octave and scale you provide?

const addOctaveNumbers = (scale, octaveNumber) => scale.map(note => {
  const firstOctaveNoteIndex = scale.indexOf('C') !== -1 ? scale.indexOf('C') : scale.indexOf('C#')
  const noteOctaveNumber = scale.indexOf(note) < firstOctaveNoteIndex ? octaveNumber - 1 : octaveNumber;
  return `${note}${noteOctaveNumber}`
});

Ta-da! Now, what do you have here? The function takes in a scale, and an octave number then transforms it using Array.map. First, you get the first note in the octave that exists in the scale (it’ll either be C or a C#) and then append the octave number based on where the note is corresponding to the first note in the octave. Down an octave, if it’s before the first note in the scale or the same octave if it’s after the first note. Sweet! How about hearing how the scale sounds?

const AMinorScaleWithOctave = addOctaveNumbers(AMinorScale, 4);
// Output ['A3', 'B3', 'C4', 'D4', 'E4', 'F4', 'G4'];

AMinorScaleWithOctave.forEach((note, index) => {
  synth.triggerAttackRelease(note, '4n', index + 1)
});

A lot should be clear at this point. However, you'll now need to provide a third param to the triggerAttackRelease method. This indicates what time you should play the note; you don't want to play them all at once! You're using the index of the Array so that the third param makes the notes play one by one.

Now that you know how to make a scale, how about learning how to make a chord? A chord is usually a collection of 3 or more notes that sound good when played together. The most common types of chords are major and minor.

Let’s focus on the major ones. To create a major chord, you need a root note, a 3rd note (which becomes two semitones away), and a 5th note (two semitones away from the third). These three notes make a "Major triad." This sounds like a perfect place to put coding skills to good use. Let's create a function that would generate major triads based on the root note, the octave, and the scale.

const constructMajorChord = (scale, octave, rootNote) => {
  const scaleWithOctave = addOctaveNumbers(scale, octave);

  const getNextChordNote = (note, nextNoteNumber) => {
    const nextNoteInScaleIndex = scaleWithOctave.indexOf(note) + nextNoteNumber - 1;
    let nextNote;
    if (typeof(scaleWithOctave[nextNoteInScaleIndex]) !== 'undefined') {
      nextNote = scaleWithOctave[nextNoteInScaleIndex];
    } else {
      nextNote = scaleWithOctave[nextNoteInScaleIndex - 7];
      const updatedOctave = parseInt(nextNote.slice(1)) + 1;
      nextNote = `${nextNote.slice(0,1)}${updatedOctave}`;
    }

    return nextNote;
  }

  const thirdNote = getNextChordNote(rootNote, 3)
  const fifthNote = getNextChordNote(rootNote, 5)
  const chord = [rootNote, thirdNote, fifthNote]

  return chord
}

Okay, so first, you need to get the scale with the octave numbers. Let's reuse the function you created beforehand. Now you need to get the next chord note. Normally you’d just add 2 or 4 to the root note index and get the next chord notes no problem. However, the issue here is that you’re also dealing with octaves, so the next note in the scale may be out of bounds. To counter this issue, simply go back to the start of the scale and then up an octave (i.e., add 1 to the octave number), thus preserving the chord. Now you know how to get the notes to take the third and fifth notes and return the chord as an Array. You could expand here if you like with error handling if you provide a note which doesn't exist in the provided scale, or perhaps an incorrectly formed scale. However, for simplicity’s sake, let’s leave the function as is here.

Produce a song in just 6 steps.

Having gone over the setup and how to create scales and chords, you're now ready to create a song! Producing a full song takes a long time. So, let’s try making a part of a song with the knowledge you’ve gained and the functions you’ve implemented above. Choose whatever genre you like. For me, I’m choosing electronic music, something with a melodic dubstep feeling.

1. Set the BPM (beats per minute).

To start, decide what the BPM for the track is going to be. BPM is short for beats per minute, and it's a basic indicator for the tempo of the track. Melodic dubstep is typically around 140-150 BPM. Let's go with 150. To set the BPM, use the Transport object you used earlier like so:

Tone.Transport.bpm.value = 150

2. Create main chords and chord progressions.

Once you have the BPM, you’re ready to build a chord progression. After creating the chord progression, you’ll get a general feel for the track you're trying to make and perhaps add a melody on top.

First, pick a scale. To make things simple, let’s stick to the A minor scale created earlier. For the chords themselves, choosing the chords for the progression can take some experimenting, listening to what sounds good together, etc. I've gone ahead and created some commonly used ones you can call upon for reference:

const IChord = constructMajorChord(AMinorScale, 4, 'A3');
// Output: ['A3', 'C4', 'E4']
const VChord = constructMajorChord(AMinorScale, 4, 'E4');
// Output: ['E4', 'G4', 'B4']
const VIChord = constructMajorChord(AMinorScale, 3, 'F3');
// Output: ['F3', 'A3', 'C3']
const IVChord = constructMajorChord(AMinorScale, 3, 'D3');
// Output: ['D3', 'F3', 'A3']

Here you’re using the chord construction function defined earlier to create the A major, E major, F major, and D major chords. If you’re new to music theory, the names of the chords might look a little confusing. They’re commonly labeled with Roman numerals depending on the position of their root note in the scale. For example, A is the first note; A major is the first chord in scale. E is the 5th note; E major is also the 5th chord.

After choosing the chords, here’s how to use a synth to play them. A simple synth won’t cut it because you’re going to be playing multiple notes at a single time. Luckily, Tone.js has you covered. Let’s use A PolySynth like so:

const synth = new Tone.PolySynth(3, Tone.Synth, {
  oscillator : {
    type : "sawtooth"
  }
}).toMaster();

This should look a little similar to the synth you created earlier, but there's a lot more going on here. PolySynth is not actually a synth; it's more of a wrapper that can produce multiple tones at once for a provided instrument. One argument is the number of "voices" or tones the provided instrument produces at a single time. Another argument is the instrument you want to use (in the case, it'll be a basic synth). Additional arguments are the volume, attack, release, and other parameters.

Next, here’s how to configure an oscillator (the little device that's responsible for producing the sound). Specify the type of oscillator as "sawtooth." Oscillators use sound waves to create noise, and the type specifies the shape of the soundwave to use. Different shapes = different sounds. Some basic shapes include square, triangle, sawtooth, and sine. In this demo, you’re creating a multi-voice synth that produces sound as a sawtooth sound wave. Sweet!

How to create music with Tone.js

Now that you have the chords and the synth, here’s how to add the actual progression or sequence. Define this sequence as an Array of objects like so:

const mainChords = [
  {'time': 0, 'note': IChord, 'duration': '2n.'},
  {'time': '0:3', 'note': VChord, 'duration': '4n'},
  {'time': '1:0', 'note': VIChord, 'duration': '2n.'},
  {'time': '1:3', 'note': VChord, 'duration': '4n'},
  {'time': '2:0', 'note': IVChord, 'duration': '2n.'},
  {'time': '2:3', 'note': VChord, 'duration': '4n'},
  {'time': '3:0', 'note': VIChord, 'duration': '2n'},
  {'time': '3:2', 'note': VChord, 'duration': '4n'},
  {'time': '3:3', 'note': IVChord, 'duration': '4n'},
  {'time': '4:0', 'note': IChord, 'duration': '2n.'},
  {'time': '4:3', 'note': VChord, 'duration': '4n'},
  {'time': '5:0', 'note': VIChord, 'duration': '2n.'},
  {'time': '5:3', 'note': VChord, 'duration': '4n'},
  {'time': '6:0', 'note': IVChord, 'duration': '2n.'},
  {'time': '6:3', 'note': VChord, 'duration': '4n'},
  {'time': '7:0', 'note': VIChord, 'duration': '2n'},
  {'time': '7:2', 'note': VChord, 'duration': '4n'},
  {'time': '7:3', 'note': IVChord, 'duration': '4n'},
];

Each of the chords has a specific time for when plays and a duration. The time notation might look a little confusing. Here you’re using measures and notes, one of the many ways to specify the time to Tone.js. For a more detailed explanation of the notation, refer to the docs.

Let’s add a Part, which is a collection of events that trigger based on provided parameters. A Part is a perfect tool to encapsulate the chords into a single unit. This is where you’re going to use the Array again.

const part = new Tone.Part(function(time, note) {
  synth.triggerAttackRelease(note.note, note.duration, time);
}, mainChords).start(0);

Here’s how to create a Part with a callback function to trigger when it starts. Also, provide mainChords as a second argument, so the callback function uses the provided options. Within the callback function, you’re calling the already familiar method triggerAttackRelease and providing it the arguments it needs from the Array. Then you’re calling the start method with 0 as the argument. This simply means the Part plays 0 seconds, (i.e., plays immediately) after the Transport is triggered. Let's listen!

This sounds fine, but the chords sound a little plain.

IChord.push('A2', 'G4')
VChord.push('E2', 'G3')
VIChord.push('F2', 'E4')
IVChord.push('D2', 'C4')

To spice the sound up, try adding a bassline note (the same note as the root, but an octave lower) and a 7th note, which makes the chords more interesting. Make sure you add these notes before defining the chord progression array. When the number of notes of the chords changes, be sure to also change the number of voices of the oscillator.

const synth = new Tone.PolySynth(5, Tone.Synth, { // changed from 3 to 5
  oscillator : {
    type : "sawtooth"
  }
}).toMaster();

3. Add the main melody.

Every track needs a good melody. The process of creating a melody is similar to creating a chord progression. Play around to find what sounds good together to you, and don't forget to tailor the melody to the chord progression. Here's a nice little uplifting melody to match the chord progression.

const mainMelody = [
  {'time': 0, 'note': 'G4', 'duration': '8n'},
  {'time': '0:0:2', 'note': 'F4', 'duration': '8n'},
  {'time': '0:1', 'note': 'D4', 'duration': '8n.'},
  {'time': '0:2', 'note': 'D4', 'duration': '8n'},
  {'time': '0:2:2', 'note': 'F4', 'duration': '8n.'},
  {'time': '0:3', 'note': 'G4', 'duration': '8n'},
  {'time': '0:3:2', 'note': 'A4', 'duration': '2n'},
  {'time': '2:0', 'note': 'A4', 'duration': '8n'},
  {'time': '2:0:2', 'note': 'G4', 'duration': '8n'},
  {'time': '2:1', 'note': 'F4', 'duration': '8n'},
  {'time': '2:2', 'note': 'A4', 'duration': '8n'},
  {'time': '2:2:2', 'note': 'G4', 'duration': '8n'},
  {'time': '2:3', 'note': 'E4', 'duration': '8n'},
  {'time': '2:3:2', 'note': 'F4', 'duration': '2n'},
  {'time': '4:0', 'note': 'G4', 'duration': '8n'},
  {'time': '4:0:2', 'note': 'F4', 'duration': '8n'},
  {'time': '4:1', 'note': 'D4', 'duration': '8n'},
  {'time': '4:2', 'note': 'F4', 'duration': '8n'},
  {'time': '4:2:2', 'note': 'A4', 'duration': '8n'},
  {'time': '4:3', 'note': 'G4', 'duration': '8n'},
  {'time': '4:3:2', 'note': 'A4', 'duration': '2n'},
  {'time': '5:2:2', 'note': 'G4', 'duration': '8n'},
  {'time': '5:3', 'note': 'A4', 'duration': '8n'},
  {'time': '5:3:2', 'note': 'B4', 'duration': '8n'},
  {'time': '6:0', 'note': 'C5', 'duration': '8n'},
  {'time': '6:1', 'note': 'B4', 'duration': '8n'},
  {'time': '6:1:2', 'note': 'A4', 'duration': '8n'},
  {'time': '6:2', 'note': 'B4', 'duration': '8n'},
  {'time': '6:2:2', 'note': 'A4', 'duration': '8n'},
  {'time': '6:3', 'note': 'G4', 'duration': '8n'},
  {'time': '6:3:2', 'note': 'A4', 'duration': '1n'},
];

This uses single notes instead of chords and can use a simple Synth this time as the instrument.

const synth2 = new Tone.Synth({
  oscillator : {
    volume: 5,
    count: 3,
    spread: 40,
    type : "fatsawtooth"
  }
}).toMaster();

Woah! This looks more customized. You’re tweaking the oscillator by increasing the volume by 5 decibels to hear it over the chords clearly. You’re also specifying the count of the oscillators as three and spreading them. This simply means there are not 1, but 3 oscillators playing the same note. However, to make a chorus sound, you’re detuning the other two oscillators by spreading the voices. Finally, instead of a sawtooth, let’s use a fatsawtooth wave, which is essentially a sawtooth wave with more depth or thickness in the sound it produces. Right on! Here’s how to create a part for the melody and trigger it:

const mainMelodyPart = new Tone.Part(function(time, note) {
  synth2.triggerAttackRelease(note.note, note.duration, time);
}, mainMelody).start(0);

Both the chord and melody parts play at the same time, sweet!

4. Include the drums.

To accompany the melody and chords, let’s add a solid beat. Tone.js has many options for drum sounds—one of which uses Tone.Sampler. The Sampler uses an existing sound anywhere on the web as a tone for the synth. All you need is to provide a link to a kick or snare drum sound and call it a day. That’s a little boring, so let’s create an original drumbeat.

Let’s start off with the kick. A kick drum is nothing more than a sine sound wave triggered with a very quick attack and fast release at a very low octave. Tone.js has a nifty synth for this occasion, the MembraneSynth. This synth creates a slightly processed sine wave with a short attack and release. Here’s how to use it:

const kickDrum = new Tone.MembraneSynth({
  volume: 6
}).toMaster();

Here’s how to create a kick pattern:

const kicks = [
  { time: '0:0' },
  { time: '0:3:2' },
  { time: '1:1' },
  { time: '2:0' },
  { time: '2:1:2' },
  { time: '2:3:2' },
  { time: '3:0:2' },
  { time: '3:1:' },
  { time: '4:0' },
  { time: '4:3:2' },
  { time: '5:1' },
  { time: '6:0' },
  { time: '6:1:2' },
  { time: '6:3:2' },
  { time: '7:0:2' },
  { time: '7:1:' },
];

const kickPart = new Tone.Part(function(time) {
  kickDrum.triggerAttackRelease('C1', '8n', time)
}, kicks).start(0);

Notice how you’re not specifying the notes or the duration of the kicks in the config Array? Why’s that? All of the kicks are featured on the same note and take the same amount of time to complete.

Now, let’s add in the snare using the NoiseSynth this time. The NoiseSynth produces different types of static noises (e.g., white, brown, pink, etc.). Here’s how to create the noise synth and customize it:

const lowPass = new Tone.Filter({
  frequency: 8000,
}).toMaster();

const snareDrum = new Tone.NoiseSynth({
  volume: 5,
  noise: {
    type: 'white',
    playbackRate: 3,
  },
  envelope: {
    attack: 0.001,
    decay: 0.20,
    sustain: 0.15,
    release: 0.03,
  },
}).connect(lowPass);

Okay. There's a lot going here. Let's break things down, starting with the noise synth itself moving up to the filter.

Let’s use NoiseSynth to specify the type of noise you want to produce and the playbackRate (i.e., the playback speed). The envelope object contains options related to the amplitude of the sound produced. Drums usually have a quick attack and release.

Now, what's this sustain and decay business? Sustain specifies what volume of noise to maintain while the note is held down. Decay indicates how long it should take to reach that level.

How to create music with Tone.js

Right, after creating the synth, you’re not connecting it to master. Instead, connect the synth to a lowPass filter. The Filter object in Tone.js is fairly simple. By default, the tool filters out frequencies that are higher than 8000Hz to get rid of any unwanted, annoying high-frequency noises the synth produces (i.e., a low-pass filter). Once the filter is connected to the synth, the snare drum is ready to roll.

const snares = [
  { time: '0:2' },
  { time: '1:2' },
  { time: '2:2' },
  { time: '3:2' },
  { time: '4:2' },
  { time: '5:2' },
  { time: '6:2' },
  { time: '7:2' },
]

const snarePart = new Tone.Part(function(time) {
  snareDrum.triggerAttackRelease('4n', time)
}, snares).start(0);

5. Bring in the bass.

The little track is really coming together! Though like any good track, it still needs a bass line. Typically, baselines use a sine wave. Let’s make one using the triangle oscillator.

const bassline = [
  {'time': 0, 'note': 'A0', 'duration': '2n'},
  {'time': '0:3', 'note': 'F0', 'duration': '2n.'},
  {'time': '1:3', 'note': 'D0', 'duration': '2n.'},
  {'time': '2:3', 'note': 'F0', 'duration': '1:1'},
];

const bass = new Tone.Synth({
  oscillator : {
    type : "triangle"
  }
}).toMaster();

const bassPart = new Tone.Part(function(time, note) {
  bass.triggerAttackRelease(note.note, note.duration, time);
}, bassline).start(0);

6. Put in the finishing touches.

Oooh. The track is almost done. The music could still sound a little better by adding a second line of chords that’s quieter and an octave higher.

const IChord1 = constructMajorChord(AMinorScale, 5, 'A4');
const VChord1 = constructMajorChord(AMinorScale, 5, 'E5');
const VIChord1= constructMajorChord(AMinorScale, 4, 'F4');
const IVChord1 = constructMajorChord(AMinorScale, 4, 'D4');

const highOctaveChords = [
  {'time': 0, 'note': IChord1, 'duration': '2n.'},
  {'time': '0:3', 'note': VChord1, 'duration': '4n'},
  {'time': '1:0', 'note': VIChord1, 'duration': '2n.'},
  {'time': '1:3', 'note': VChord1, 'duration': '4n'},
  {'time': '2:0', 'note': IVChord1, 'duration': '2n.'},
  {'time': '2:3', 'note': VChord1, 'duration': '4n'},
  {'time': '3:0', 'note': VIChord1, 'duration': '2n'},
  {'time': '3:2', 'note': VChord1, 'duration': '4n'},
  {'time': '3:3', 'note': IVChord1, 'duration': '4n'},
  {'time': '4:0', 'note': IChord1, 'duration': '2n.'},
  {'time': '4:3', 'note': VChord1, 'duration': '4n'},
  {'time': '5:0', 'note': VIChord1, 'duration': '2n.'},
  {'time': '5:3', 'note': VChord1, 'duration': '4n'},
  {'time': '6:0', 'note': IVChord1, 'duration': '2n.'},
  {'time': '6:3', 'note': VChord1, 'duration': '4n'},
  {'time': '7:0', 'note': VIChord1, 'duration': '2n'},
  {'time': '7:2', 'note': VChord1, 'duration': '4n'},
  {'time': '7:3', 'note': IVChord1, 'duration': '4n'},
];

const highSynth = new Tone.PolySynth(5, Tone.Synth, {
  volume: -16,
  count: 6,
  spread: 80,
  oscillator : {
    type : "fatsawtooth"
  }
}).toMaster();

const highOctaveChordPart = new Tone.Part(function(time, note) {
  highSynth.triggerAttackRelease(note.note, note.duration, time, 0.5);
}, highOctaveChords).start(0);

Let’s try making the original chord synth a little quieter to help the main melody pop out a little more.

const synth = new Tone.PolySynth(5, Tone.Synth, {
  volume: -5, // Added
  oscillator : {
    type : "sawtooth"
  }
}).toMaster();

Excellent! That’s how to create a small track. Now, let’s listen!

Improvement ideas

Hopefully, this guide shows you the ropes of creating music with Tone.js. You don't have to stop here. There's a lot you can still do to enhance the track you’re creating.

With the melodic dubstep genre, why not add an iconic dubstep growl and bassy "wubs"? The drums stand alone. However, there are plenty of more percussion elements that can be added, such as high-hats, cymbals, and crashes.

You can also expand the track by making an intro, a build-up, a lead, and an ending. In this case, the code piles up very quickly. Splitting the track into separate files might be a good idea.

If making a song isn’t your cup of tea, switch it around. Try a virtual instrument using the synths Tone.js provides. How about testing out a drum machine or maybe a midi sequencer? The possibilities are endless! Make some wonderful music, keep learning, and remember to have fun along the way!

Additional resources

Tone.js is an incredibly extensive framework. This guide barely scratches the surface of what you can do with it. For more information on the framework, look at the documentation filled with working examples and explanations.

If this guide got you interested in music production, I highly recommend checking out Andrew Huang’s short explanation of music theory.

You can also find the full code for this guide here.