About a year ago, a few co-workers and I decided that we'd all participate in js13k. If you're not familiar, js13k is a game competition where each entry has to be smaller than 13 KB.

Being a musician, it felt important that my game would have music. After looking around a bit, I wasn't able to find any libraries that really did what I wanted without totally blowing the 13 KB budget. So I decided to make my own.

This post is the first of two parts. In it, I'll explain the Note class in TinyMusic. We'll dig in to a little bit of light music theory and math.

In part two, I'll cover the Sequence class, and talk about the Web Audio API.

TinyMusic

Before I start digging in, here's a link to the final product: TinyMusic. You can also check out a small demo here.

Keep in mind that this is a fairly bare-bones library. It was really purpose-built for something like js13k, so the idea was to deliver the minimum useful feature set for basic music synthesis.

Notes

The first thing I started work on was a Note class.

var note = new Note('A4 q');  

This was the API that I wanted. Basically, it accepts a string containing a note name and a time value (q = quarter, h = half, etc.), separated by a space.

In order to translate the note name into a frequency, you need two things:

  1. A known frequency for a given note.
  2. A formula for moving between your known frequency and the target note.
Choosing a starting frequency

Most musicians probably know that A4 = 440Hz, so that seems like a good choice for our target frequency. But since numbered octaves actually begin at C, we really want to use middle C (C4) as our target.

Knowing that C4 is 9 half-steps below A4, we can get its frequency like this:

var middleC = 440 * Math.pow( Math.pow( 2, 1 / 12 ), -9 );  

I won't dig too far into the math here, but hopefully you recognize the 12 (number of semitones in an octave) and the -9 (the distance between A4 and C4).

We'll be using the same formula later on to derive all of our other frequencies.

Converting note names to frequencies

Now that we have our target frequency for middle C, we need a way to translate other note names into frequencies.

The first thing I did was create a string of every note and then use that to populate an offsets hash.

var enharmonics = 'B#-C|C#-Db|D|D#-Eb|E-Fb|E#-F|F#-Gb|G|G#-Ab|A|A#-Bb|B-Cb';  
var offsets = {};

enharmonics.split('|').forEach(function( val, i ) {  
  val.split('-').forEach(function( note ) {
    offsets[ note ] = i;
  });
});

The groupings in that string are kind of important to understand. The basic idea is that in music theory, sometimes there are actually multiple names for the same note. So B# and C are equivalent. Fb and E are equivalent. You get the idea...

Anyway, what we end up with at the end of that process is an object where each key is a note name (A, F#, Db, etc.), and the value is the distance between that note and C (in half-steps).

From there, we just need a little bit of math.

var num = /(\d+)/;  
// the octave number of our known frequency, middle C
var octaveOffset = 4;

Note.getFrequency = function( name ) {  
  var couple = name.split( num ),
    distance = offsets[ couple[ 0 ] ],
    octaveDiff = ( couple[ 1 ] || octaveOffset ) - octaveOffset,
    freq = middleC * Math.pow( Math.pow( 2, 1 / 12 ), distance );
  return freq * Math.pow( 2, octaveDiff );
};

// usage:
Note.getFrequency('F3'); // 174.61  

So we're doing a few things here. The first line just creates an array that looks like [ 'F', '3' ].

From there, we get the note's distance from C by looking it up in the offsets object. In this case, it's 5.

octaveDiff ends up being -1, which means that our target frequency is in the octave below our known frequency.

Next, we calculate a frequency as if our target note was in the same octave as middle C. This is the same basic formula you saw earlier.

Finally, we shift our frequency to the correct octave.

Durations

In addition to frequency, each Note also needs a duration.

In this case, duration isn't actually measured in seconds or milliseconds. It's really just going to be a ratio of the note's time value to one beat length.

In other words, for a quarter note, duration would be 1 (because it's exactly one beat long). For a half note, it would be 2. An eighth note is 0.5, etc.

Dotted notes

If you read music, you're probably familiar with "dotted" notes.

If not, it basically means that when a note is dotted, you add 50% to its duration. So a dotted eighth note is really like an eighth note plus a sixteenth note.

It was pretty important to support this in TinyMusic, so this is how I did it:

var note = new Note('A4 es');  

Here, the e stands for "eighth" and the s for "sixteenth". When I parse that string, I simply add the two durations together.

Numeric values

There are so many possible values for note duration that I couldn't realistically support all of them with short-hand strings (e.g. "q" = quarter, "e" = eighth, etc).

So the Note constructor also accepts numeric values, like this.

var note = new Note('A4 0.75');  
Putting it all together

This is the actual function I ended up with.

var numeric = /^[0-9.]+$/;

Note.getDuration = function( symbol ) {  
  return numeric.test( symbol ) ? parseFloat( symbol ) :
    symbol.toLowerCase().split('').reduce(function( prev, curr ) {
      return prev + ( curr === 'w' ? 4 : curr === 'h' ? 2 :
        curr === 'q' ? 1 : curr === 'e' ? 0.5 :
        curr === 's' ? 0.25 : 0 );
    }, 0 );
};

I know, the nested ternary thing is gross. But in my defense, I wrote this a while ago and I was trying to save bytes. So whatever.

Anyway, here's how this thing works:

  1. If we got a string that looks like a number, just call parseFloat and return the number.
  2. Otherwise, loop through each character in the string and try to get a time value for it – then add them up.

The second step is accomplished with Array#reduce. This is what gives us the ability to support strings like 'es'. We just get the value for e and add it to the value for s.

Wrapping up

Now that we've got our Note class, we need a way to group them together and play them. I accomplish this with a class called Sequence, which I'll be covering in part 2 tomorrow.


Follow me on Twitter or Medium for more posts. I’m trying to write once a day for the next 30 days.

And if you’re in the Boston area and want to come work on crazy, top-secret things with me at Project Decibel, shoot me an email. I’m hiring.