730aecc076
Remodel application to support the scientific pitch notation for notes. Instead of supporting simply "C", support "C4". This change created cascading changes. After refactoring for around an hour, I restored the app to a working state. The current state is not desirable, but it compiles. More changes on the way.
271 lines
10 KiB
Elm
271 lines
10 KiB
Elm
module Theory exposing (..)
|
|
|
|
import List.Extra
|
|
import Misc
|
|
|
|
{-| Notes are the individuals sounds that we use to create music. Think: "do re
|
|
mi fa so la ti do".
|
|
|
|
Note: Technically a "C-sharp" is also a "D-flat", but I will model accidentals
|
|
(i.e. sharps and flats) as sharps and represent the ambiguity when I render the
|
|
underlying state of the application.
|
|
|
|
Note: There are "notes" like A, B, D-flat, and then there are notes like "middle
|
|
C", also denoted in scientific pitch notation as C4. I'm unsure of what to call
|
|
each of these, and my application does not model scientific pitch notation yet,
|
|
so these non-scientific pitch denote values are "notes" for now. -}
|
|
type Note = C1 | C_sharp1 | D1 | D_sharp1 | E1 | F1 | F_sharp1 | G1 | G_sharp1 | A1 | A_sharp1 | B1
|
|
| C2 | C_sharp2 | D2 | D_sharp2 | E2 | F2 | F_sharp2 | G2 | G_sharp2 | A2 | A_sharp2 | B2
|
|
| C3 | C_sharp3 | D3 | D_sharp3 | E3 | F3 | F_sharp3 | G3 | G_sharp3 | A3 | A_sharp3 | B3
|
|
| C4 | C_sharp4 | D4 | D_sharp4 | E4 | F4 | F_sharp4 | G4 | G_sharp4 | A4 | A_sharp4 | B4
|
|
| C5 | C_sharp5 | D5 | D_sharp5 | E5 | F5 | F_sharp5 | G5 | G_sharp5 | A5 | A_sharp5 | B5
|
|
| C6 | C_sharp6 | D6 | D_sharp6 | E6 | F6 | F_sharp6 | G6 | G_sharp6 | A6 | A_sharp6 | B6
|
|
| C7 | C_sharp7 | D7 | D_sharp7 | E7 | F7 | F_sharp7 | G7 | G_sharp7 | A7 | A_sharp7 | B7
|
|
| C8
|
|
|
|
{-| I alluded to this concept in the Note type's documentation. These are the
|
|
letters of notes. For instance C2, C3, C4 are all instances of C. -}
|
|
type NoteClass = C
|
|
| C_sharp
|
|
| D
|
|
| D_sharp
|
|
| E
|
|
| F
|
|
| F_sharp
|
|
| G
|
|
| G_sharp
|
|
| A
|
|
| A_sharp
|
|
| B
|
|
|
|
{-| Encode whether you are traversing "up" or "down" intervals -}
|
|
type StepDirection = Up | Down
|
|
|
|
{-| One can measure the difference between between notes using intervals. -}
|
|
type Interval = Half
|
|
| Whole
|
|
| MajorThird
|
|
| MinorThird
|
|
|
|
{-| A bundle of notes which are usually, but not necessarily harmonious. -}
|
|
type alias Chord =
|
|
{ note : Note
|
|
, chordType : ChordType
|
|
, chordPosition : ChordPosition
|
|
}
|
|
|
|
{-| Many possible chords exist. This type encodes the possibilities. I am
|
|
tempted to model these in a more "DRY" way, but I worry that this abstraction
|
|
may cause more problems than it solves. -}
|
|
type ChordType = Major
|
|
| Major7
|
|
| MajorDominant7
|
|
| Minor
|
|
| Minor7
|
|
| MinorDominant7
|
|
| Augmented
|
|
| Augmented7
|
|
| Diminished
|
|
| Diminished7
|
|
|
|
{-| On a piano, a triad can be played three ways. As a rule-of-thumb, The number
|
|
of ways a pianist can play a chord is equal to the number of notes in the chord
|
|
itself. -}
|
|
type ChordPosition = First
|
|
| Second
|
|
| Third
|
|
| Fourth
|
|
|
|
{-| Songs are written in one or more keys, which define the notes and therefore
|
|
chords that harmonize with one another. -}
|
|
type alias Key =
|
|
{ noteClass : NoteClass
|
|
, mode : Mode
|
|
}
|
|
|
|
{-| We create "scales" by enumerating the notes of a given key. These keys are
|
|
defined by the "tonic" note and the "mode". I thought about including Ionian,
|
|
Dorian, Phrygian, etc., but in the I would like to avoid over-abstracting this
|
|
early on, so I'm going to err on the side of overly concrete until I have a
|
|
better idea of the extent of this project. -}
|
|
type Mode = BluesMode
|
|
| MajorMode
|
|
| MinorMode
|
|
|
|
{-| Returns the Note in the cental octave of the piano for a given
|
|
NoteClass. For example, C4 -- or "middle C" -- for C. -}
|
|
noteInCentralOctave : NoteClass -> Note
|
|
noteInCentralOctave noteClass =
|
|
case noteClass of
|
|
C -> C4
|
|
C_sharp -> C_sharp4
|
|
D -> D4
|
|
D_sharp -> D_sharp4
|
|
E -> E4
|
|
F -> F4
|
|
F_sharp -> F_sharp4
|
|
G -> G4
|
|
G_sharp -> G_sharp4
|
|
A -> A4
|
|
A_sharp -> A_sharp4
|
|
B -> B4
|
|
|
|
{-| Return the note that is one half step away from `note` in the direction,
|
|
`dir`.
|
|
In the case of stepping up or down from the end of the piano, this returns a
|
|
Maybe.
|
|
-}
|
|
halfStep : StepDirection -> Note -> Maybe Note
|
|
halfStep dir note =
|
|
case dir of
|
|
Up -> Misc.comesAfter note allNotes
|
|
Down -> Misc.comesBefore note allNotes
|
|
|
|
{-| Return a list of steps to take away from the root note to return back to the
|
|
root note for a given mode.
|
|
-}
|
|
intervalsForMode : Mode -> List Interval
|
|
intervalsForMode mode =
|
|
case mode of
|
|
MajorMode -> [Whole, Whole, Half, Whole, Whole, Whole, Half]
|
|
MinorMode -> [Whole, Half, Whole, Whole, Half, Whole, Whole]
|
|
BluesMode -> [MinorThird, Whole, Half, Half, MinorThird]
|
|
|
|
{-| Return a list of the intervals the comprise a chord -}
|
|
intervalsForChordType : ChordType -> List Interval
|
|
intervalsForChordType chordType =
|
|
case chordType of
|
|
Major -> [MajorThird, MinorThird]
|
|
Major7 -> [MajorThird, MinorThird, MajorThird]
|
|
MajorDominant7 -> [MajorThird, MinorThird, MajorThird, MinorThird]
|
|
Minor -> [MinorThird, MajorThird]
|
|
Minor7 -> [MinorThird, MajorThird, MajorThird]
|
|
MinorDominant7 -> [MinorThird, MajorThird, MajorThird, MinorThird]
|
|
Augmented -> [MajorThird, MajorThird]
|
|
Augmented7 -> [MajorThird, MajorThird, Whole]
|
|
Diminished -> [MinorThird, MinorThird]
|
|
Diminished7 -> [MinorThird, MinorThird, MinorThird]
|
|
|
|
{-| Return the note in the direction, `dir`, away from `note` `s` intervals -}
|
|
step : StepDirection -> Interval -> Note -> Maybe Note
|
|
step dir s note =
|
|
let
|
|
doHalfStep = halfStep dir
|
|
in
|
|
case s of
|
|
Half ->
|
|
doHalfStep note
|
|
Whole ->
|
|
doHalfStep note
|
|
|> Maybe.andThen doHalfStep
|
|
MinorThird ->
|
|
doHalfStep note
|
|
|> Maybe.andThen doHalfStep
|
|
|> Maybe.andThen doHalfStep
|
|
MajorThird ->
|
|
doHalfStep note
|
|
|> Maybe.andThen doHalfStep
|
|
|> Maybe.andThen doHalfStep
|
|
|> Maybe.andThen doHalfStep
|
|
|
|
{-| Returns a list of all of the notes up from a give `note`.
|
|
|
|
In the case where applying all of the steps would result in running off of the
|
|
edge of the piano, this function returns a Maybe. -}
|
|
applySteps : List Interval -> Note -> Maybe (List Note)
|
|
applySteps steps note =
|
|
doApplySteps steps note [] |> Maybe.map List.reverse
|
|
|
|
doApplySteps : List Interval -> Note -> List Note -> Maybe (List Note)
|
|
doApplySteps steps note result =
|
|
case steps of
|
|
[] -> Just (note::result)
|
|
s::rest ->
|
|
case step Up s note of
|
|
Just x -> doApplySteps rest x (note::result)
|
|
Nothing -> Nothing
|
|
|
|
{-| Return the NoteClass for a given note. -}
|
|
classifyNote : Note -> NoteClass
|
|
classifyNote note =
|
|
if List.member note [C1, C2, C3, C4, C5, C6, C7, C8] then
|
|
C
|
|
else if List.member note [C_sharp1, C_sharp2, C_sharp3, C_sharp4, C_sharp5, C_sharp6, C_sharp7] then
|
|
C_sharp
|
|
else if List.member note [D1, D2, D3, D4, D5, D6, D7] then
|
|
D
|
|
else if List.member note [D_sharp1, D_sharp2, D_sharp3, D_sharp4, D_sharp5, D_sharp6, D_sharp7] then
|
|
D_sharp
|
|
else if List.member note [E1, E2, E3, E4, E5, E6, E7] then
|
|
E
|
|
else if List.member note [F1, F2, F3, F4, F5, F6, F7] then
|
|
F
|
|
else if List.member note [F_sharp1, F_sharp2, F_sharp3, F_sharp4, F_sharp5, F_sharp6, F_sharp7] then
|
|
F_sharp
|
|
else if List.member note [G1, G2, G3, G4, G5, G6, G7] then
|
|
G
|
|
else if List.member note [G_sharp1, G_sharp2, G_sharp3, G_sharp4, G_sharp5, G_sharp6, G_sharp7] then
|
|
G_sharp
|
|
else if List.member note [A1, A2, A3, A4, A5, A6, A7] then
|
|
A
|
|
else if List.member note [A_sharp1, A_sharp2, A_sharp3, A_sharp4, A_sharp5, A_sharp6, A_sharp7] then
|
|
A_sharp
|
|
else
|
|
B
|
|
|
|
{-| Return a list of the notes that comprise a `chord` -}
|
|
notesForChord : Chord -> Maybe (List Note)
|
|
notesForChord {note, chordType} =
|
|
case applySteps (intervalsForChordType chordType) note of
|
|
Nothing -> Nothing
|
|
Just notes -> Just <| note::notes
|
|
|
|
{-| Return the scale for a given `key` -}
|
|
notesForKey : Key -> List Note
|
|
notesForKey {noteClass, mode} =
|
|
let origin = noteInCentralOctave noteClass
|
|
in case applySteps (intervalsForMode mode) origin of
|
|
-- We should never hit the Nothing case here.
|
|
Nothing -> []
|
|
Just scale -> scale
|
|
|
|
{-| Return a list of all of the notes that we know about. -}
|
|
allNotes : List Note
|
|
allNotes =
|
|
[ C1 , C_sharp1 , D1 , D_sharp1 , E1 , F1 , F_sharp1 , G1 , G_sharp1 , A1 , A_sharp1 , B1
|
|
, C2 , C_sharp2 , D2 , D_sharp2 , E2 , F2 , F_sharp2 , G2 , G_sharp2 , A2 , A_sharp2 , B2
|
|
, C3 , C_sharp3 , D3 , D_sharp3 , E3 , F3 , F_sharp3 , G3 , G_sharp3 , A3 , A_sharp3 , B3
|
|
, C4 , C_sharp4 , D4 , D_sharp4 , E4 , F4 , F_sharp4 , G4 , G_sharp4 , A4 , A_sharp4 , B4
|
|
, C5 , C_sharp5 , D5 , D_sharp5 , E5 , F5 , F_sharp5 , G5 , G_sharp5 , A5 , A_sharp5 , B5
|
|
, C6 , C_sharp6 , D6 , D_sharp6 , E6 , F6 , F_sharp6 , G6 , G_sharp6 , A6 , A_sharp6 , B6
|
|
, C7 , C_sharp7 , D7 , D_sharp7 , E7 , F7 , F_sharp7 , G7 , G_sharp7 , A7 , A_sharp7 , B7
|
|
, C8
|
|
]
|
|
|
|
{-| Return a list of all of the chords that we know about. -}
|
|
allChords : List Chord
|
|
allChords =
|
|
let notes = allNotes
|
|
chordTypes = [ Major
|
|
, Major7
|
|
, MajorDominant7
|
|
, Minor
|
|
, Minor7
|
|
, MinorDominant7
|
|
, Augmented
|
|
, Augmented7
|
|
, Diminished
|
|
, Diminished7
|
|
]
|
|
chordPositions = [ First
|
|
, Second
|
|
, Third
|
|
, Fourth
|
|
] in
|
|
notes
|
|
|> List.Extra.andThen (\note -> chordTypes
|
|
|> List.Extra.andThen (\chordType -> chordPositions
|
|
|> List.Extra.andThen (\chordPosition -> [{ note = note
|
|
, chordType = chordType
|
|
, chordPosition = chordPosition
|
|
}])))
|