This is the second part of the music theory in F# series:
There are 12 notes in an octave, but if I play random notes, the sound of the melody will be closer to a piano falling down the stairs than a Beethoven sonata. Fortunately, scales will help us.
The most famous scale is the major scale. A scale has a starting note named root and other notes after it. In example the C major scale is:
C, D, E, F, G, A, B
A scale is defined by the intervals between each notes. To create a major scale, choose a root note and add these intervals:
Whole tone, semitone, whole tone, whole tone, whole tone, semitone
For instance, to create a C major:
Root: C Second: D = C + 2 semitones Third: E = D + 2 semitones Fourth: F = E + 1 semitone Fifth: G = F + 2 semitones Sixth: A = G + 2 semitones Seventh: B = A + 2 semitones Octave: C = B + 1 semitone
Each note of the scale is named after its position (root, second, third…). You can now create any major scale by applying these intervals. Here is another example, E major:
Root: E Second: F# = E + 2 semitones Third: G# = F# + 2 semitones Fourth: A = G# + 1 semitone Fifth: B = A + 2 semitones Sixth: C# = B + 2 semitones Seventh: D# = C# + 2 semitones Octave: E = D# + 1 semitone
And so on…
We can start by designing the
MajorScale type and its module.
type MajorScale = MajorScale of Note module MajorScale = let private intervals = [ 2; 2; 1; 2; 2; 2; 1 ]
Remember the union type for the
Note type? It can be seen as an enum, but unions are more than that because each case can have its own data:
type Shape = | Point | Square of int | Rectangle of int * int // int * int is the type for a tuple of int and int // All these values are Shape let point = Point let square = Square 3 let rect = Rectangle (4, 5) // (4, 5) is a value for a tuple of int and int
MajorScale is a single union. It’s a trick to wrap a type into another one with a clear intention. More explanations in this article.
Now we want a
notes function that takes a
MajorScale and return the list of its
Notes. In the theory part, we saw that the algorithm to find all the notes behave like this:
Return the root note foreach interval in intervals return the previous note + interval
This function already exists, it is called
List.scan. It takes 3 parameters:
- The list to iterate (
- The starting point (
- The function to compute the new value from the current item and the previous value (
module MajorScale = let private intervals = [ 2; 2; 1; 2; 2; 2; 1 ] let notes (MajorScale root) = let addSemitones note interval = // not implemented yet intervals |> List.scan addSemitones root // expected behavior let cMajorNotes = MajorScale C |> notes // [ C; D; E; F; G; A; B ]
notes takes a
MajorScale as parameter and destructures it. By doing this, we can easily access to
root of the scale.
addSemitones is a function that takes a note and an interval and return the new note:
// expected behavior let dSharp = addSemitones D 1 // DSharp
With this function implemented,
notes called on a
MajorScale C will return:
[ C; D; E; F; G; A; B; C ]
C appears two times because the last interval
1 is the interval from the seventh to the octave. We don’t need it to get all the notes, so we can remove the last interval:
module MajorScale = let private intervals = [ 2; 2; 1; 2; 2; 2; 1 ] let notes (MajorScale root) = let addSemitones note interval = // not implemented yet intervals |> List.take (intervals.Length - 1) |> List.scan addSemitones root
module Note = let addSemitones note semitones = // find the index where the item of the list equals note let index = all |> List.findIndex ((=) note) // add the semitones to the index // and get only the rest of the division let newIndex = (index + semitones) % all.Length all[newIndex]
and call this function in the
module MajorScale = let notes (MajorScale root) = intervals |> List.take (intervals.Length - 1) |> List.scan Note.addSemitones root
At the end of the script, we can print the notes of the scale:
// Add this helper on top of the file, just after the open statements module String = let joinWith (separator: string) (strings: string list) = String.Join(separator, strings) MajorScale randomNote |> MajorScale.notes |> List.map Note.toString |> String.joinWith ", " |> printfn "Notes of the major scale: %s" // With randomNote = C, it prints: // Notes of the major scale: C, D, E, F, G, A, B
Yay, now we can play random notes in this scale and it sound good!
That’s all for this step. You can check out the full code here.
Next step: Chords of the Major scale