about summary refs log tree commit diff
path: root/website/sandbox/chord-drill-sergeant/src/Theory.elm
blob: 9e84aeac4c4e09bb82bc6e9af77d38c27d853ba6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
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
                                             }])))