10.2. Musical Scales and ArraysΒΆ

Music in the western classical tradition uses a twelve-tone chromatic scale. Any of the tones in this scale can be the basis of a major scale. Most musicians (especially pianists) learn the C-major scale in the early days of study, owing to the ability to play this scale entirely with the ivory (white) keys.

The following declaration shows how to initialize an array consisting of the twelve tones of the chromatic scale, starting from the C note.

1
2
3
static string[] tones = { "C", "C#", "D", "D#", "E", "F",
                          "F#", "G", "G#", "A", "A#", "B"
                        };

Even if you’re not a musician, learning the basic principles is fairly straightforward.

The well-known C-major scale, which is often sung as:

Do Re Mi Fa So La Ti Do

has the following progression:

C  D  E  F  G  A  B  C

This progression is known as the diatonic major scale. If you look at the tones array, you can actually figure out the intervals associated with this array:

C + 2 = D
D + 2 = E
E + 1 = F
F + 2 = G
G + 2 = A
A + 2 = B
B + 1 = C

So given any starting note, the major scale can be generated from the intervals (represented as an array).

So, for example, if you want the F-major scale, you can get it by starting at F and applying the steps of 2, 2, 1, 2, 2, 2, 1:

F + 2 = G
G + 2 = A
A + 1 = B' (flat) a.k.a. A#)
B'+ 2 = C
C + 2 = D
D + 2 = E
E + 1 = F

So this is the F-major scale:

F G A B' C D E F

We begin by creating a helper function, FindTone(), which does a linear search to find the key of the scale we want to compute. The aim is to make it easy for the user to just specify the key of interest. Then we can use this position to compute the scale given the major (or minor, covered shortly) interval array.

1
2
3
4
5
6
7
static int FindTone(string key) {
   for (int i=0; i < tones.GetLength(0); i++) {
      if (key == tones[i])
         return i;
   }
   return -1;
}

To see what this function does, pick your favorite key (C and G are very common for beginners).

  • FindTone("C") gives 0, the first position in the tones array.
  • FindTone("G") gives 8.

For example, C is the first note in the array of tones, so FindTone("C") would give us 0. FindTone("F") would give us 6.

So let’s take a look at ComputeScale() which does the work of computing a scale, given a key and an array of steps. The scale array is allocated by the Main() method, primarily to allow the same array to be used repeatedly for calculating other scales.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static void ComputeScale(string key, int[] steps, int[] scale) {
   int tonePosition = 0;
   int startTone;

   startTone = FindTone(key);
   if (startTone < 0)
      return;
   if (steps.GetLength(0)+1 != scale.GetLength(0))
      return;
   tonePosition = startTone;
   for (int i=0; i < steps.GetLength(0); i++) {
      scale[i] = tonePosition % tones.GetLength(0);
      tonePosition += steps[i];
   }
}
  1. The first thing to note is the setup of this code. We’re going to keep the startTone (obtained by calling FindTone()) and tonePosition, which is the note we are presently visiting in the tones array.
  2. Remember that every scale (e.g. C, D, F#, etc.) can always be obtained by looking at tones and using the appropriate intervals (the steps parameter) to compute the next note, given a current note.
  3. We do some simple checks in line 6 (to ensure that a valid key was specified by the caller) and in line 8 to ensure that the number of steps + 1 is the length of the scale–and the length of the scale is 8. (We technically don’t have to limit the scale to 8, because scales can keep going until you run out of playable notes on the instrument.)
  4. We’ll now start at the initial position (where we found the base note of the key) and enter a for loop to compute all of the notes in the scale. This loop iterates over the entries in the steps array to decide what the next note is.
  5. The next note in the scale, scale[i] is computed by taking tonePosition % tones.GetLength(0). We need to do this, because in most scales, you will eventually end up “falling off the end” of the tones array, which mens that you need to continue computing notes from the beginning of the array. You can inspect this for yourself by picking a scale (say, B) that is starting at the end of the tones array. This means you will need to go to the beginning of the array to get C# (which is 2 tones away from B).
  6. The next note is found by adding steps[i] to tonePosition.

The following function writes the scale out (rather naively) by just printing the notes from our existing tones array.

1
2
3
4
5
6
static void WriteScale(int[] scale) {
   foreach (int i in scale) {
      Console.Write ("{0} ", tones[i]);
   }
   Console.WriteLine ();
}

We say that the output is naive because any musician will tell you that a scale should be printed in a normalized way. For example, the F-major scale (shown above in our earlier explanation) is never written with A# as one of its notes. It is written as B-flat. It’s easy to manage the various cases by consulting the circle of fifths, which gives us guidance on the number of flats/sharps each scale has.

Lastly, we put this all together.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static void Main (string[] args)
{
   int[] scale = new int[8];
   int[] major = { 2, 2, 1, 2, 2, 2, 1 };
   int[] minor = { 2, 1, 2, 2, 1, 2, 2 };

   string name = args[0];  // need command line tone name
   Console.WriteLine("{0} major scale", name);
   ComputeScale(name, major, scale);
   WriteScale(scale);
   Console.WriteLine("{0} minor scale", name);
   ComputeScale(name, minor, scale);
   WriteScale(scale);
}

This Main() method shows how to set up the steps for both major and minor scales. We’ve already explained how to express the steps of a major scale. The minor scale basically drops the 3rd and 7th by a semitone (a single step), which gives us a different pattern.

You can run this program to see the major and minor scales.