Incidentally, I love Music Theory, Math, and F#, so I couldn't resist exploring this problem.
At first, I attempted a purely functional solution, using only modules, F# functions, and basic data-structures, but this quickly spun out of control (since I sought some rather ambitious goals including supporting arbitrary scales, not just "major" and "minor"). What follows is my first "serious" effort at "programming in the medium" in F# using object orientation. As I said before, I thought I could avoid this, but it turned out that using object orientation in F# actually works out quite nice, and doesn't undermine the beauty and succinctness too much (especially when we disregard consumability by other .NET languages).
Utils.fs
For starters, I have a couple utility functions I will be using:
module MusicTheory.Utils
open System
let rotate (arr:_[]) start =
[|start..arr.Length + start - 1|] |> Array.map (fun i -> arr.[i% arr.Length])
//http://stackoverflow.com/questions/833180/handy-f-snippets/851449#851449
let memoize f =
let cache = Collections.Generic.Dictionary<_,_>(HashIdentity.Structural)
fun x ->
match cache.TryGetValue(x) with
| true, res -> res
| _ -> let res = f x
cache.[x] <- res
res
Note.fs
The Note type encapsulates a music note including it's name, sign (NoteSign), and it's position relative to other notes. But it does little other than that. The Aux module holds some underlying data-structures used for constructing and validating Notes (note that I am not too found of this module, I would have rather used private static fields on the Note type, but F# doesn't support private static fields. And since I am using a namespace instead of module to hold my types (so that I can use top of file declarations), I can't use free floating let bindings). I think the pattern match for extracting the NoteSign is especially neat.
namespace MusicTheory
open Utils
open System
///want to use public static field on Note, but don't exist
module Aux =
let indexedNoteNames =
let arr = [|
["B#"; "C"] //flip this order?
["C#";"Db"]
["D"]
["D#";"Eb"]
["E";"Fb"]
["E#";"F" ] //flip this order?
["F#";"Gb"]
["G"]
["G#";"Ab"]
["A"]
["A#";"Bb"]
["B";"Cb"]
|]
Array.AsReadOnly(arr)
let noteNames = indexedNoteNames |> Seq.concat |> Seq.toList
let indexedSignlessNoteNames = [|'A';'B';'C';'D';'E';'F';'G'|]
open Aux
type NoteSign =
| Flat
| Sharp
| Natural
//Represents a note name and it's relative position (index)
type Note(name:string) =
let name =
match noteNames |> List.exists ((=) name) with
| true -> name
| false -> failwith "invalid note name: %s" name
let sign =
match name |> Seq.toArray with
| [|_|] -> NoteSign.Natural
| [|_;'#'|] -> NoteSign.Sharp
| [|_;'b'|] -> NoteSign.Flat
| _ -> failwith "invalid note name sign" //not possible
let index =
indexedNoteNames
|> Seq.findIndex (fun names -> names |> List.exists ((=) name))
with
member self.Name = name
member self.SignlessName = name.[0]
member self.Sign = sign
member self.Index = index
override self.ToString() = name
override self.GetHashCode() = name.GetHashCode()
override self.Equals(other:obj) =
match other with
| :? Note as otherNote -> otherNote.Name = self.Name
| _ -> false
///memoized instances of Note
static member get = memoize (fun name -> Note(name))
Pitch.fs
Next is Pitch which encapsulates a specific frequency in the chromatic scale relative to some starting point, 0 (C). It exposes calculations for which octave it lays in as well as the set of Notes which may describe it (noting that outside of the context of a scale starting at a specific Note, are equally valid).
namespace MusicTheory
open Utils
open Aux
open System
///A note is a value 0-11 corresponding to positions in the chromatic scale.
///A pitch is any value relative to a starting point of the chromatic scale
type Pitch (pitchIndex:int) =
let pitchIndex = pitchIndex
let noteIndex = Math.Abs(pitchIndex % 12)
let octave =
if pitchIndex >= 0 then (pitchIndex / 12) + 1
else (pitchIndex / 12) - 1
let notes = indexedNoteNames.[noteIndex] |> List.map Note.get
with
member self.Notes = notes
member self.PitchIndex = pitchIndex
member self.NoteIndex = noteIndex
///e.g. pitchIndex = 5 -> 1, pitchIndex = -5 -> -1, pitchIndex = 13 -> 2
member self.Octave = octave
override self.ToString() = sprintf "Notes = %A, PitchIndex = %i, NoteIndex = %i, Octave = %i" notes noteIndex pitchIndex octave
override self.GetHashCode() = pitchIndex
override self.Equals(other:obj) =
match other with
| :? Pitch as otherPitch -> otherPitch.PitchIndex = self.PitchIndex
| _ -> false
///memoized instances of Pitch
static member get = memoize (fun index -> Pitch(index))
///get the first octave pitch for the given note
static member getByNote (note:Note) = note.Index |> Pitch.get
///get the first octave pitch for the given note name
static member getByNoteName name = name |> Note.get |> Pitch.getByNote
ScaleIntervals.fs
In anticipation of our upcoming Scale type, we have a module ScaleIntervals filled with sub-modules filled with lists of intervals between pitches which describe scales (note that this differs from the index based representation others have been using). For your interest, note that Mode.ionian and Mode.aeolian correspond to the "major" and "minor" scales respectively. In practice, you'd probably want to use some external means for loading scale intervals at runtime.
//could encapsulate as a type, instead of checking in Scale constructors
///define modes by chromatic interval sequence
module MusicTheory.ScaleIntervals
open Utils
module Mode =
let ionian = [|2;2;1;2;2;2;1|] //i.e. "Major"
let dorian = Utils.rotate ionian 1
let phrygian = Utils.rotate ionian 2
let lydian = Utils.rotate ionian 3
let mixolydian = Utils.rotate ionian 4
let aeolian = Utils.rotate ionian 5 //i.e. "Minor
let locrian = Utils.rotate ionian 6
module EqualTone =
let half = [|1;1;1;1;1;1;1;1;1;1;1;1|]
let whole = [|2;2;2;2;2;2|]
module Pentatonic =
let major = [|2;2;3;2;3|]
let minor = Utils.rotate major 4 //not sure
Scale.fs
Here lays the heart of our solution. Itself, a Scale is quite simple, merely wrapping a sequence of scale intervals. But when viewed in the context of a Pitch or a Note, yields all of our results. I will point out that in isolation of a Pitch or a Note, Scale does have the interesting feature that it yields an infinite sequence of RelativeIndices derived from the scale intervals. Using this, we can yield an infinite sequence of Pitches built from this Scale starting at a given Pitch (GetPitches). But now for the most interesting method: GetNotePitchTuples, which yields on infinite sequence of Note, Pitch tuples, where the Notes are heuristically selected (see comments on that method for more info). Scale also provides several overloads for getting at Note sequences more easily, including a ToString(string) overload which accepts a string Note name and returns a string listing the first octave of Note names.
namespace MusicTheory
open Utils
open System
///A Scale is a set of intervals within an octave together with a root pitch
type Scale(intervals:seq<int>) =
let intervals =
if intervals |> Seq.sum <> 12 then
failwith "intervals invalid, do not sum to 12"
else
intervals
let relativeIndices =
let infiniteIntervals = Seq.initInfinite (fun _ -> intervals) |> Seq.concat
infiniteIntervals |> Seq.scan (fun pos cur -> pos+cur) 0
with
member self.Intervals = intervals
member self.RelativeIndices = relativeIndices
override self.ToString() = sprintf "%A" intervals
override self.GetHashCode() = intervals.GetHashCode()
override self.Equals(other:obj) =
match other with
| :? Scale as otherScale -> otherScale.Intervals = self.Intervals
| _ -> false
///Infinite sequence of pitches for this scale starting at rootPitch
member self.GetPitches(rootPitch:Pitch) =
relativeIndices
|> Seq.map (fun i -> Pitch.get (rootPitch.PitchIndex + i))
///Infinite sequence of Note, Pitch tuples for this scale starting at rootPitch.
///Notes are selected heuristically: works perfectly for Modes, but needs some work
///for Pentatonic and EqualTone (perhaps introduce some kind of Sign bias or explicit classification).
member self.GetNotePitchTuples(rootNote:Note, rootPitch:Pitch) =
let selectNextNote (prevNote:Note) (curPitch:Pitch) =
//make sure octave note same as root note
if curPitch.Notes |> List.exists ((=) rootNote) then
rootNote
else
//take the note with the least distance (signless name wise) from the root note
//but not if the distance is 0. assumes curPitch.Notes ordered asc in this way.
//also assumes that curPitch.Notes of length 1 or 2.
match curPitch.Notes with
| [single] -> single
| [first;second] when first.SignlessName = prevNote.SignlessName -> second
| [first;_] -> first
self.GetPitches(rootPitch)
|> Seq.scan
(fun prev curPitch ->
match prev with
| None -> Some(rootNote, rootPitch) //first
| Some(prevNote,_) -> Some(selectNextNote prevNote curPitch, curPitch)) //subsequent
None
|> Seq.choose id
member self.GetNotePitchTuples(rootNote:Note) =
self.GetNotePitchTuples(rootNote, Pitch.getByNote rootNote)
member self.GetNotePitchTuples(rootNoteName:string) =
self.GetNotePitchTuples(Note.get rootNoteName)
///return a string representation of the notes of this scale in an octave for the given note
member self.ToString(note:Note) =
let notes =
(Scale(intervals).GetNotePitchTuples(note))
|> Seq.take (Seq.length intervals + 1)
|> Seq.toList
|> List.map (fst)
sprintf "%A" notes
///return a string representation of the notes of this scale in an octave for the given noteName
member self.ToString(noteName:string) =
self.ToString(Note.get noteName)
Here is a demonstration:
open MusicTheory
open Aux
open ScaleIntervals
let testScaleNoteHeuristics intervals =
let printNotes (noteName:string) =
printfn "%A" (Scale(intervals).ToString(noteName))
noteNames
|> Seq.iter printNotes
//> testScaleNoteHeuristics Mode.ionian;;
//"[B#; D; E; F; G; A; B; B#]"
//"[C; D; E; F; G; A; B; C]"
//"[C#; D#; E#; F#; G#; A#; B#; C#]"
//"[Db; Eb; F; Gb; Ab; Bb; C; Db]"
//"[D; E; F#; G; A; B; C#; D]"
//"[D#; E#; G; Ab; Bb; C; D; D#]"
//"[Eb; F; G; Ab; Bb; C; D; Eb]"
//"[E; F#; G#; A; B; C#; D#; E]"
//"[Fb; Gb; Ab; A; B; C#; D#; Fb]"
//"[E#; G; A; Bb; C; D; E; E#]"
//"[F; G; A; Bb; C; D; E; F]"
//"[F#; G#; A#; B; C#; D#; E#; F#]"
//"[Gb; Ab; Bb; Cb; Db; Eb; F; Gb]"
//"[G; A; B; C; D; E; F#; G]"
//"[G#; A#; B#; C#; D#; E#; G; G#]"
//"[Ab; Bb; C; Db; Eb; F; G; Ab]"
//"[A; B; C#; D; E; F#; G#; A]"
//"[A#; B#; D; Eb; F; G; A; A#]"
//"[Bb; C; D; Eb; F; G; A; Bb]"
//"[B; C#; D#; E; F#; G#; A#; B]"
//"[Cb; Db; Eb; Fb; Gb; Ab; Bb; Cb]"
//val it : unit = ()
Chords
The next step is supporting the concept of a chord, both in isolation from a Scale (a set of Pitches) and in context of a Scale with a given root Note. I haven't given too much thought into whether any encapsulation is warranted here, but it would be very straight-forward to enhance Scale to, say, return a progression of chords (e.g. Note list for each Note in scale) given a starting Note and a chord pattern (like a triad, for example).