about summary refs log tree commit diff
path: root/website/sandbox/chord-drill-sergeant/src/Main.elm
diff options
context:
space:
mode:
authorWilliam Carroll <wpcarro@gmail.com>2020-04-11T09·45+0100
committerWilliam Carroll <wpcarro@gmail.com>2020-04-11T09·45+0100
commit3562343c196b381fea7ebff9fc8612ac0ad927ff (patch)
tree6c6132c50a35556f657f3c936cd624cf58100085 /website/sandbox/chord-drill-sergeant/src/Main.elm
parent3dac2f10ff4f7f6c5f82cacd27e09c079dcd0367 (diff)
Generate all known chords and display randomly selected chords
First of all, Elm's purity is beautiful. I think every language should model
their error messages and develop experience after Elm. If I didn't have to
download packages, I don't think I would need an internet connection to
troubleshoot my program's errors. This is how helpful I find the compiler.

Now that that's out of the way, here's what I've changed since we've last
corresponded:
- Use Elm's Browser.element to create a reactive application with state
- Write a function to generate all of the chords about which CDS knows
- Move some code out of Main.elm into other modules
- Depend on List.Extra, Random, Random.Extra

What's left:
- Lots of work
- Instead of clicking a button to show a new chord, use a timer
- Add mobile-first styling (probably add TailwindCSS)
- Persist settings in LocalStorage (and then eventually create user accounts)
- Allow users to curate the list of chords they're interested in practicing
- Deploy the website and dogfood it

Unknowns:
- How can I handle tempo? I don't expect setInterval to be enough (maybe it
  is)...
Diffstat (limited to 'website/sandbox/chord-drill-sergeant/src/Main.elm')
-rw-r--r--website/sandbox/chord-drill-sergeant/src/Main.elm275
1 files changed, 84 insertions, 191 deletions
diff --git a/website/sandbox/chord-drill-sergeant/src/Main.elm b/website/sandbox/chord-drill-sergeant/src/Main.elm
index 4fdb30226baf..8878a7ceecf1 100644
--- a/website/sandbox/chord-drill-sergeant/src/Main.elm
+++ b/website/sandbox/chord-drill-sergeant/src/Main.elm
@@ -4,203 +4,96 @@ import Browser
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Html.Events exposing (..)
+import Random
+import Random.List
 
 import Piano
-
-{-| 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 = C
-          | C_sharp
-          | D
-          | D_sharp
-          | E
-          | F
-          | F_sharp
-          | G
-          | G_sharp
-          | A
-          | A_sharp
-          | B
-
-{-| 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
-
-{-| One can measure the difference between between notes using intervals. -}
-type Interval = Half
-              | Whole
-              | MajorThird
-              | MinorThird
-
-{-| Songs are written in one or more keys, which define the notes and therefore
-chords that harmonize with one another. -}
-type Key = Key (Note, Mode)
-
-{-| A bundle of notes which are usually, but not necessarily harmonious. -}
-type Chord = Chord (Note, ChordType, ChordPosition)
-
-{-| 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
-
-{-| 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
-
-{-| Encode whether you are traversing "up" or "down" intervals -}
-type StepDirection = Up | Down
-
-{-| 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 -> Note
-step dir s note =
-  let
-    doHalfStep = halfStep dir
-  in
-    case s of
-      Half       -> doHalfStep note
-      Whole      -> doHalfStep note |> doHalfStep
-      MinorThird -> doHalfStep note |> doHalfStep |> doHalfStep
-      MajorThird -> doHalfStep note |> doHalfStep |> doHalfStep |> doHalfStep
-
-{-| Return the note that is one half step away from `note` in the direction,
-`dir`.
--}
-halfStep : StepDirection -> Note -> Note
-halfStep dir note =
-  case (dir, note) of
-    -- C
-    (Up, C) -> C_sharp
-    (Down, C) -> B
-    -- C#
-    (Up, C_sharp) -> D
-    (Down, C_sharp) -> C
-    -- D
-    (Up, D) -> D_sharp
-    (Down, D) -> C_sharp
-    -- D_sharp
-    (Up, D_sharp) -> E
-    (Down, D_sharp) -> D
-    -- E
-    (Up, E) -> F
-    (Down, E) -> D_sharp
-    -- F
-    (Up, F) -> F_sharp
-    (Down, F) -> E
-    -- F#
-    (Up, F_sharp) -> G
-    (Down, F_sharp) -> F
-    -- G
-    (Up, G) -> G_sharp
-    (Down, G) -> F_sharp
-    -- G#
-    (Up, G_sharp) -> A
-    (Down, G_sharp) -> A
-    -- A
-    (Up, A) -> A_sharp
-    (Down, A) -> G_sharp
-    -- A#
-    (Up, A_sharp) -> B
-    (Down, A_sharp) -> A
-    -- B
-    (Up, B) -> C
-    (Down, B) -> A_sharp
-
-{-| Returns a list of all of the notes up from a give `note` -}
-applySteps : List Interval -> Note -> List Note
-applySteps steps note =
-  case List.foldl (\s (prev, result) -> ((step Up s prev), (step Up s prev :: result))) (note, []) steps of
-    (_, result) -> List.reverse result
-
-{-| Return the scale for a given `key` -}
-notesForKey : Key -> List Note
-notesForKey key =
-  case key of
-    Key (note, mode) -> applySteps (intervalsForKeyMode mode) note
-
-{-| Return a list of the notes that comprise a `chord` -}
-notesForChord : Chord -> List Note
-notesForChord chord =
-  case chord of
-    -- TODO(wpcarro): Use the Position to rotate the chord n times
-    Chord (note, chordType, _) -> applySteps (intervalsForChordType chordType) note
+import Theory
+
+type State = State { whitelistedChords : List Theory.Chord
+                   , selectedChord : Theory.Chord
+                   }
+
+type Msg = NextChord
+         | NewChord Theory.Chord
+
+viewChord : Theory.Chord -> String
+viewChord (Theory.Chord (note, chordType, chordPosition)) =
+  viewNote note ++ " " ++
+  (case chordType of
+     Theory.Major -> "major"
+     Theory.Major7 -> "major 7th"
+     Theory.MajorDominant7 -> "major dominant 7th"
+     Theory.Minor -> "minor"
+     Theory.Minor7 -> "minor 7th"
+     Theory.MinorDominant7 -> "minor dominant 7th"
+     Theory.Augmented -> "augmented"
+     Theory.Augmented7 -> "augmented 7th"
+     Theory.Diminished -> "diminished"
+     Theory.Diminished7 -> "diminished 7th") ++ " " ++
+  (case chordPosition of
+     Theory.First -> "root position"
+     Theory.Second -> "2nd position"
+     Theory.Third -> "3rd position"
+     Theory.Fourth -> "4th position")
 
 {-| Serialize a human-readable format of `note` -}
-viewNote : Note -> String
+viewNote : Theory.Note -> String
 viewNote note =
   case note of
-    C -> "C"
-    C_sharp -> "C♯/D♭"
-    D -> "D"
-    D_sharp -> "D♯/E♭"
-    E -> "E"
-    F -> "F"
-    F_sharp -> "F♯/G♭"
-    G -> "G"
-    G_sharp -> "G♯/A♭"
-    A -> "A"
-    A_sharp -> "A♯/B♭"
-    B -> "B"
+    Theory.C -> "C"
+    Theory.C_sharp -> "C♯/D♭"
+    Theory.D -> "D"
+    Theory.D_sharp -> "D♯/E♭"
+    Theory.E -> "E"
+    Theory.F -> "F"
+    Theory.F_sharp -> "F♯/G♭"
+    Theory.G -> "G"
+    Theory.G_sharp -> "G♯/A♭"
+    Theory.A -> "A"
+    Theory.A_sharp -> "A♯/B♭"
+    Theory.B -> "B"
+
+cmajor : Theory.Chord
+cmajor = Theory.Chord (Theory.C, Theory.Major, Theory.First)
+
+{-| The initial state for the application. -}
+initialState : State
+initialState =
+  State { whitelistedChords = Theory.allChords
+        , selectedChord = cmajor
+        }
+
+{-| Now that we have state, we need a function to change the state. -}
+update : Msg -> State -> (State, Cmd Msg)
+update msg (State {whitelistedChords, selectedChord}) =
+  case msg of
+    NewChord chord -> ( State { whitelistedChords = whitelistedChords
+                              , selectedChord = chord
+                              }
+                      , Cmd.none
+                      )
+    NextChord -> ( State { whitelistedChords = whitelistedChords
+                         , selectedChord = selectedChord
+                         }
+                 , Random.generate (\x ->
+                                      case x of
+                                        (Just chord, _) -> NewChord chord
+                                        (Nothing, _)    -> NewChord cmajor)
+                   (Random.List.choose whitelistedChords)
+                 )
+
+view : State -> Html Msg
+view (State {selectedChord}) =
+  div [] [ p [] [ text (viewChord selectedChord) ]
+         , button [ onClick NextChord ] [ text "Next Chord" ]
+         , Piano.render { highlight = [] }
+         ]
 
 {-| For now, I'm just dumping things onto the page to sketch ideas. -}
 main =
-  let
-    key = Key (D, MinorMode)
-    chord = Chord (D, Major, First)
-  in
-    div [] [ ul [] (notesForKey key |> List.map (\n -> li [] [ text (viewNote n) ]))
-           , ul [] (notesForChord chord |> List.map (\n -> li [] [ text (viewNote n) ]))
-           , Piano.render
-           ]
+  Browser.element { init = \() -> (initialState, Cmd.none)
+                  , subscriptions = \_ -> Sub.none
+                  , update = update
+                  , view = view
+                  }