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 Note
s (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 Note
s 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 Pitche
s 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 Note
s 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 Pitche
s) 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).