about summary refs log blame commit diff
path: root/users/wpcarro/website/sandbox/learnpianochords/src/Theory.elm
blob: 7f54832c97a0aaa3f500b24a4bf79ed5ed6cd0bb (plain) (tree)
1
2
3
4
5
6
7
8
9

                           
                             
                           
                 
                  
           
 
 









                                                                                

























































































                                                                

                                                                             

                                                                 
               













































                                                                         
                  




                                     


                                                                             



                                       

          










                         


                                                                                













                                                       

                                                                               

                                       
                
                             


                 

                                                                              
                                                                             

                                                                              







                                          


                         
                             



                    



                                                                
 
 
                                                                  
                                                      
  


                                        



































                    
 














                                                           







                                                      





                         






























                                     

                                                                            

                                                                            
  
                                              
                   










                                           
 


                                                                                
                                              
                       








                                              
                                                                   

                    
                                                                  




                                                                     
                                                                       





















                                                                          



















                                                   





















































































































                                                                                 
                                           





















































                                                                          


                                                            
 
 



                                                                           
                 





                                 
                                                       



                           
 










                                           
                                           
  
                                 
                   
                                          
 


                                                      
                                          





                                                     
 
                                       
  
                              
                                  

                
                                          
      
                                                    











                                                     
                            





                                         
                                       






                                                         


                         



                                            











                                                                     

          












                         
























                                                              



                                                                               


                                 










                                                                                          
                                                                                          










                                                                                          
                                                                                          










                                                                                          
                                                                                          










                                                                                          
                                                                                          










                                                                                          
                                                                                          










                                                                                          
                                                                                          










                                                                                          
                                                                                          
                                                                     



                                                                   
  









                                                                          

 

                                                                         
  

                          








                                                          
             
 

                        
























                                                                               
                                                                 
  

                                 























                                                                             
                                    

                 
                                                                


                                    
                                                                                      




















                                                                      
                                             



                                     

 

                                              

                                               
                                                                                                                              

 
                                                   
  


                                     


































                       



























































































                                                                                
module Theory exposing (..)

import Array exposing (Array)
import Dict exposing (Dict)
import List.Extra
import Maybe.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 PitchClass
    = 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
    | NHalves Int
    | Whole
    | MajorThird
    | MinorThird
    | PerfectFifth
    | AugmentedFifth
    | DiminishedFifth
    | MajorSeventh
    | DominantSeventh


{-| Add direction to a distance on the piano.
-}
type alias IntervalVector =
    { interval : Interval
    , direction : StepDirection
    }


{-| A bundle of notes which are usually, but not necessarily harmonious.
-}
type alias Chord =
    { note : Note
    , chordType : ChordType
    , chordInversion : ChordInversion
    }


{-| 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
    | Sus2
    | Sus4
    | Major7
    | MajorDominant7
    | Minor
    | MinorMajor7
    | MinorDominant7
    | Augmented
    | AugmentedDominant7
    | Diminished
    | DiminishedDominant7
    | DiminishedMajor7


{-| 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 ChordInversion
    = Root
    | First
    | Second


{-| Whether a given note is a white key or a black key.
-}
type KeyClass
    = Natural
    | Accidental


{-| Songs are written in one or more keys, which define the notes and therefore
chords that harmonize with one another.
-}
type alias Key =
    { pitchClass : PitchClass
    , 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


type alias NoteMetadata =
    { note : Note
    , label : String
    , pitchClass : PitchClass
    , natural : Bool
    }


{-| An integer representing which note in a given scale to play.
-}
type alias ScaleDegree =
    Int


{-| Returns the Note in the cental octave of the piano for a given
PitchClass. For example, C4 -- or "middle C" -- for C.
-}
noteInCentralOctave : PitchClass -> Note
noteInCentralOctave pitchClass =
    case pitchClass 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 human-readable version of a chord inversion.
-}
inversionName : ChordInversion -> String
inversionName inversion =
    case inversion of
        Root ->
            "Root"

        First ->
            "First"

        Second ->
            "Second"


{-| Return the human-readable version of a chord type.
-}
chordTypeName : ChordType -> String
chordTypeName chordType =
    case chordType of
        Major ->
            "major"

        Sus2 ->
            "suspended 2"

        Sus4 ->
            "suspended 4"

        Major7 ->
            "major 7th"

        MajorDominant7 ->
            "major dominant 7th"

        Minor ->
            "minor"

        MinorMajor7 ->
            "minor major 7th"

        MinorDominant7 ->
            "minor dominant 7th"

        Augmented ->
            "augmented"

        AugmentedDominant7 ->
            "augmented dominant 7th"

        Diminished ->
            "diminished"

        DiminishedDominant7 ->
            "diminished dominant 7th"

        DiminishedMajor7 ->
            "diminished major 7th"


{-| 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 =
    let
        everyNote =
            notesFromRange C2 C8
    in
    case dir of
        Up ->
            Misc.comesAfter note everyNote

        Down ->
            Misc.comesBefore note everyNote


{-| 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 IntervalVector
intervalsForMode mode =
    let
        up x =
            { direction = Up, interval = x }

        down x =
            { direction = Down, interval = x }
    in
    case mode of
        MajorMode ->
            List.map up [ Whole, Whole, Half, Whole, Whole, Whole ]

        MinorMode ->
            List.map up [ Whole, Half, Whole, Whole, Half, Whole ]

        BluesMode ->
            List.map up [ MinorThird, Whole, Half, Half, MinorThird ]


{-| Return a list of the intervals that a chord. Each interval measures
the distance away from the root-note of the chord.
-}
intervalsForChordType : ChordType -> ChordInversion -> List IntervalVector
intervalsForChordType chordType chordInversion =
    let
        up x =
            { direction = Up, interval = x }

        down x =
            { direction = Down, interval = x }
    in
    case ( chordType, chordInversion ) of
        -- Major
        ( Major, Root ) ->
            [ up MajorThird, up PerfectFifth ]

        ( Major, First ) ->
            [ down (NHalves 5), down (NHalves 8) ]

        ( Major, Second ) ->
            [ down (NHalves 5), up MajorThird ]

        -- Sus2
        ( Sus2, Root ) ->
            [ up Whole, up PerfectFifth ]

        ( Sus2, First ) ->
            [ down (NHalves 10), down (NHalves 5) ]

        ( Sus2, Second ) ->
            [ down (NHalves 5), up Whole ]

        -- Sus4
        ( Sus4, Root ) ->
            [ up (NHalves 5), up PerfectFifth ]

        ( Sus4, First ) ->
            [ down (NHalves 7), down (NHalves 5) ]

        ( Sus4, Second ) ->
            [ down (NHalves 5), up (NHalves 5) ]

        -- Major7
        ( Major7, Root ) ->
            [ up MajorThird, up PerfectFifth, up MajorSeventh ]

        ( Major7, First ) ->
            down Half :: intervalsForChordType Major chordInversion

        ( Major7, Second ) ->
            down Half :: intervalsForChordType Major chordInversion

        -- MajorDominant7
        ( MajorDominant7, Root ) ->
            up DominantSeventh :: intervalsForChordType Major chordInversion

        ( MajorDominant7, First ) ->
            down Whole :: intervalsForChordType Major chordInversion

        ( MajorDominant7, Second ) ->
            down Whole :: intervalsForChordType Major chordInversion

        -- Minor
        ( Minor, Root ) ->
            [ up MinorThird, up PerfectFifth ]

        ( Minor, First ) ->
            [ down (NHalves 5), down (NHalves 9) ]

        ( Minor, Second ) ->
            [ down (NHalves 5), up MinorThird ]

        -- MinorMajor7
        ( MinorMajor7, Root ) ->
            up MajorSeventh :: intervalsForChordType Minor chordInversion

        ( MinorMajor7, First ) ->
            down Half :: intervalsForChordType Minor chordInversion

        ( MinorMajor7, Second ) ->
            down Half :: intervalsForChordType Minor chordInversion

        -- MinorDominant7
        ( MinorDominant7, Root ) ->
            up DominantSeventh :: intervalsForChordType Minor chordInversion

        ( MinorDominant7, First ) ->
            down Whole :: intervalsForChordType Minor chordInversion

        ( MinorDominant7, Second ) ->
            down Whole :: intervalsForChordType Minor chordInversion

        -- Augmented
        ( Augmented, Root ) ->
            [ up MajorThird, up AugmentedFifth ]

        ( Augmented, First ) ->
            [ down (NHalves 8), down (NHalves 4) ]

        ( Augmented, Second ) ->
            [ down (NHalves 4), up MajorThird ]

        -- AugmentedDominant7
        ( AugmentedDominant7, Root ) ->
            up DominantSeventh :: intervalsForChordType Augmented chordInversion

        ( AugmentedDominant7, First ) ->
            down Whole :: intervalsForChordType Augmented chordInversion

        ( AugmentedDominant7, Second ) ->
            down Whole :: intervalsForChordType Augmented chordInversion

        -- Diminished
        ( Diminished, Root ) ->
            [ up MinorThird, up DiminishedFifth ]

        ( Diminished, First ) ->
            [ down (NHalves 6), down (NHalves 9) ]

        ( Diminished, Second ) ->
            [ down (NHalves 6), up MinorThird ]

        -- DiminishedDominant7
        ( DiminishedDominant7, Root ) ->
            up DominantSeventh :: intervalsForChordType Diminished chordInversion

        ( DiminishedDominant7, First ) ->
            down Whole :: intervalsForChordType Diminished chordInversion

        ( DiminishedDominant7, Second ) ->
            down Whole :: intervalsForChordType Diminished chordInversion

        -- DiminishedMajor7
        ( DiminishedMajor7, Root ) ->
            up MajorSeventh :: intervalsForChordType Diminished chordInversion

        ( DiminishedMajor7, First ) ->
            down Half :: intervalsForChordType Diminished chordInversion

        ( DiminishedMajor7, Second ) ->
            down Half :: intervalsForChordType Diminished chordInversion


{-| Return the note in the direction, `dir`, away from `note` `s` intervals
-}
step : IntervalVector -> Note -> Maybe Note
step { direction, interval } note =
    let
        doStep int =
            step { direction = direction, interval = int }
    in
    case interval of
        Half ->
            halfStep direction note

        NHalves n ->
            List.repeat n
                { direction = direction
                , interval = Half
                }
                |> (\x -> walkNotes x note)
                |> Maybe.andThen (List.reverse >> List.head)

        Whole ->
            note
                |> doStep Half
                |> Maybe.andThen (doStep Half)

        MinorThird ->
            note
                |> doStep Whole
                |> Maybe.andThen (doStep Half)

        MajorThird ->
            note
                |> doStep Whole
                |> Maybe.andThen (doStep Whole)

        PerfectFifth ->
            note
                |> doStep MajorThird
                |> Maybe.andThen (doStep MinorThird)

        AugmentedFifth ->
            note
                |> doStep PerfectFifth
                |> Maybe.andThen (doStep Half)

        DiminishedFifth ->
            note
                |> doStep MajorThird
                |> Maybe.andThen (doStep Whole)

        MajorSeventh ->
            note
                |> doStep PerfectFifth
                |> Maybe.andThen (doStep MajorThird)

        DominantSeventh ->
            note
                |> doStep PerfectFifth
                |> Maybe.andThen (doStep MinorThird)


{-| Returns a list of all of the notes away from a give `note`.

  - The 0th element is applied to `note`.
  - The 1st element is applied to the result of the previous operation.
  - The 2nd element is applied to the result of the previous operation.
  - and so on...until all of the `steps` are exhausted.

In the case where applying any of the steps would result in running off of
either edge of the piano, this function returns a Nothing.

-}
walkNotes : List IntervalVector -> Note -> Maybe (List Note)
walkNotes steps note =
    doWalkNotes steps note [] |> Maybe.map List.reverse


{-| Recursive helper for `walkNotes`.
-}
doWalkNotes : List IntervalVector -> Note -> List Note -> Maybe (List Note)
doWalkNotes steps note result =
    case steps of
        [] ->
            Just (note :: result)

        s :: rest ->
            case step s note of
                Just x ->
                    doWalkNotes rest x (note :: result)

                Nothing ->
                    Nothing


{-| Return the KeyClass for a given `note`.
-}
keyClass : Note -> KeyClass
keyClass note =
    if isNatural note then
        Natural

    else
        Accidental


{-| Return the PitchClass for a given note.
-}
classifyNote : Note -> PitchClass
classifyNote note =
    note |> getNoteMetadata |> .pitchClass


{-| Return a list of the notes that comprise a `chord`
-}
notesForChord : Chord -> Maybe (List Note)
notesForChord { note, chordType, chordInversion } =
    intervalsForChordType chordType chordInversion
        |> List.map (\interval -> step interval note)
        |> Maybe.Extra.combine
        |> Maybe.map (\notes -> note :: notes)


{-| Return the scale for a given `key`.
-}
notesForKey : Key -> List Note
notesForKey { pitchClass, mode } =
    let
        origin =
            noteInCentralOctave pitchClass
    in
    case walkNotes (intervalsForMode mode) origin of
        -- We should never hit the Nothing case here.
        Nothing ->
            []

        Just scale ->
            scale


{-| Return true if `note` is a black key.
-}
isAccidental : Note -> Bool
isAccidental note =
    note |> isNatural |> not


{-| Return true if `note` is a white key.
-}
isNatural : Note -> Bool
isNatural note =
    note |> getNoteMetadata |> .natural


{-| Return a list of all of the notes that we know about.
Only return the notes within the range `start` and `end`.
-}
notesFromRange : Note -> Note -> List Note
notesFromRange start end =
    noteMetadata
        |> Array.toList
        |> List.map .note
        |> List.Extra.dropWhile ((/=) start)
        |> List.Extra.takeWhile ((/=) end)


{-| Return a list of all of the chord inversions about which we know.
-}
allInversions : List ChordInversion
allInversions =
    [ Root, First, Second ]


{-| Return a list of all of the chord types about which we know.
-}
allChordTypes : List ChordType
allChordTypes =
    [ Major
    , Sus2
    , Sus4
    , Major7
    , MajorDominant7
    , Minor
    , MinorMajor7
    , MinorDominant7
    , Augmented
    , AugmentedDominant7
    , Diminished
    , DiminishedDominant7
    , DiminishedMajor7
    ]


{-| Return a list of all of the key modes about which we know.
-}
allModes : List Mode
allModes =
    [ MajorMode, MinorMode, BluesMode ]


{-| Return a list of all of the keys about which we know.
-}
allKeys : List Key
allKeys =
    allPitchClasses
        |> List.Extra.andThen
            (\pitchClass ->
                allModes
                    |> List.Extra.andThen
                        (\mode ->
                            [ { pitchClass = pitchClass
                              , mode = mode
                              }
                            ]
                        )
            )


{-| Return an array of every note on a piano.
Note: Currently this piano has 85 keys, but modern pianos have 88 keys. I would
prefer to have 88 keys, but it's not urgent.
-}
noteMetadata : Array NoteMetadata
noteMetadata =
    Array.fromList
        [ { note = A1, label = "A1", pitchClass = A, natural = True }
        , { note = A_sharp1, label = "A♯/B♭1", pitchClass = A_sharp, natural = False }
        , { note = B1, label = "B1", pitchClass = B, natural = True }
        , { note = C1, label = "C1", pitchClass = C, natural = True }
        , { note = C_sharp1, label = "C♯/D♭1", pitchClass = C_sharp, natural = False }
        , { note = D1, label = "D1", pitchClass = D, natural = True }
        , { note = D_sharp1, label = "D♯/E♭1", pitchClass = D_sharp, natural = False }
        , { note = E1, label = "E1", pitchClass = E, natural = True }
        , { note = F1, label = "F1", pitchClass = F, natural = True }
        , { note = F_sharp1, label = "F♯/G♭1", pitchClass = F_sharp, natural = False }
        , { note = G1, label = "G1", pitchClass = G, natural = True }
        , { note = G_sharp1, label = "G♯/A♭1", pitchClass = G_sharp, natural = False }
        , { note = A2, label = "A2", pitchClass = A, natural = True }
        , { note = A_sharp2, label = "A♯/B♭2", pitchClass = A_sharp, natural = False }
        , { note = B2, label = "B2", pitchClass = B, natural = True }
        , { note = C2, label = "C2", pitchClass = C, natural = True }
        , { note = C_sharp2, label = "C♯/D♭2", pitchClass = C_sharp, natural = False }
        , { note = D2, label = "D2", pitchClass = D, natural = True }
        , { note = D_sharp2, label = "D♯/E♭2", pitchClass = D_sharp, natural = False }
        , { note = E2, label = "E2", pitchClass = E, natural = True }
        , { note = F2, label = "F2", pitchClass = F, natural = True }
        , { note = F_sharp2, label = "F♯/G♭2", pitchClass = F_sharp, natural = False }
        , { note = G2, label = "G2", pitchClass = G, natural = True }
        , { note = G_sharp2, label = "G♯/A♭2", pitchClass = G_sharp, natural = False }
        , { note = A3, label = "A3", pitchClass = A, natural = True }
        , { note = A_sharp3, label = "A♯/B♭3", pitchClass = A_sharp, natural = False }
        , { note = B3, label = "B3", pitchClass = B, natural = True }
        , { note = C3, label = "C3", pitchClass = C, natural = True }
        , { note = C_sharp3, label = "C♯/D♭3", pitchClass = C_sharp, natural = False }
        , { note = D3, label = "D3", pitchClass = D, natural = True }
        , { note = D_sharp3, label = "D♯/E♭3", pitchClass = D_sharp, natural = False }
        , { note = E3, label = "E3", pitchClass = E, natural = True }
        , { note = F3, label = "F3", pitchClass = F, natural = True }
        , { note = F_sharp3, label = "F♯/G♭3", pitchClass = F_sharp, natural = False }
        , { note = G3, label = "G3", pitchClass = G, natural = True }
        , { note = G_sharp3, label = "G♯/A♭3", pitchClass = G_sharp, natural = False }
        , { note = A4, label = "A4", pitchClass = A, natural = True }
        , { note = A_sharp4, label = "A♯/B♭4", pitchClass = A_sharp, natural = False }
        , { note = B4, label = "B4", pitchClass = B, natural = True }
        , { note = C4, label = "C4", pitchClass = C, natural = True }
        , { note = C_sharp4, label = "C♯/D♭4", pitchClass = C_sharp, natural = False }
        , { note = D4, label = "D4", pitchClass = D, natural = True }
        , { note = D_sharp4, label = "D♯/E♭4", pitchClass = D_sharp, natural = False }
        , { note = E4, label = "E4", pitchClass = E, natural = True }
        , { note = F4, label = "F4", pitchClass = F, natural = True }
        , { note = F_sharp4, label = "F♯/G♭4", pitchClass = F_sharp, natural = False }
        , { note = G4, label = "G4", pitchClass = G, natural = True }
        , { note = G_sharp4, label = "G♯/A♭4", pitchClass = G_sharp, natural = False }
        , { note = A5, label = "A5", pitchClass = A, natural = True }
        , { note = A_sharp5, label = "A♯/B♭5", pitchClass = A_sharp, natural = False }
        , { note = B5, label = "B5", pitchClass = B, natural = True }
        , { note = C5, label = "C5", pitchClass = C, natural = True }
        , { note = C_sharp5, label = "C♯/D♭5", pitchClass = C_sharp, natural = False }
        , { note = D5, label = "D5", pitchClass = D, natural = True }
        , { note = D_sharp5, label = "D♯/E♭5", pitchClass = D_sharp, natural = False }
        , { note = E5, label = "E5", pitchClass = E, natural = True }
        , { note = F5, label = "F5", pitchClass = F, natural = True }
        , { note = F_sharp5, label = "F♯/G♭5", pitchClass = F_sharp, natural = False }
        , { note = G5, label = "G5", pitchClass = G, natural = True }
        , { note = G_sharp5, label = "G♯/A♭5", pitchClass = G_sharp, natural = False }
        , { note = A6, label = "A6", pitchClass = A, natural = True }
        , { note = A_sharp6, label = "A♯/B♭6", pitchClass = A_sharp, natural = False }
        , { note = B6, label = "B6", pitchClass = B, natural = True }
        , { note = C6, label = "C6", pitchClass = C, natural = True }
        , { note = C_sharp6, label = "C♯/D♭6", pitchClass = C_sharp, natural = False }
        , { note = D6, label = "D6", pitchClass = D, natural = True }
        , { note = D_sharp6, label = "D♯/E♭6", pitchClass = D_sharp, natural = False }
        , { note = E6, label = "E6", pitchClass = E, natural = True }
        , { note = F6, label = "F6", pitchClass = F, natural = True }
        , { note = F_sharp6, label = "F♯/G♭6", pitchClass = F_sharp, natural = False }
        , { note = G6, label = "G6", pitchClass = G, natural = True }
        , { note = G_sharp6, label = "G♯/A♭6", pitchClass = G_sharp, natural = False }
        , { note = A7, label = "A7", pitchClass = A, natural = True }
        , { note = A_sharp7, label = "A♯/B♭7", pitchClass = A_sharp, natural = False }
        , { note = B7, label = "B7", pitchClass = B, natural = True }
        , { note = C7, label = "C7", pitchClass = C, natural = True }
        , { note = C_sharp7, label = "C♯/D♭7", pitchClass = C_sharp, natural = False }
        , { note = D7, label = "D7", pitchClass = D, natural = True }
        , { note = D_sharp7, label = "D♯/E♭7", pitchClass = D_sharp, natural = False }
        , { note = E7, label = "E7", pitchClass = E, natural = True }
        , { note = F7, label = "F7", pitchClass = F, natural = True }
        , { note = F_sharp7, label = "F♯/G♭7", pitchClass = F_sharp, natural = False }
        , { note = G7, label = "G7", pitchClass = G, natural = True }
        , { note = G_sharp7, label = "G♯/A♭7", pitchClass = G_sharp, natural = False }
        , { note = C8, label = "C8", pitchClass = C, natural = True }
        ]


{-| Mapping of note data to commonly needed metadata for that note.
-}
getNoteMetadata : Note -> NoteMetadata
getNoteMetadata note =
    case Array.get (noteAsNumber note) noteMetadata of
        Just metadata ->
            metadata

        -- This case should never hit, so we just return C1 to appease the
        -- compiler.
        Nothing ->
            getNoteMetadata C1


{-| Return the numeric representation of `note` to ues when comparing two
notes.
-}
noteAsNumber : Note -> Int
noteAsNumber note =
    let
        result =
            noteMetadata
                |> Array.toList
                |> List.indexedMap Tuple.pair
                |> Misc.find (\( _, x ) -> x.note == note)
    in
    case result of
        Nothing ->
            0

        Just ( i, _ ) ->
            i


{-| Return true if all of the notes that comprise `chord` can be played on a
piano whose keys begin at `start` and end at `end`.
-}
chordWithinRange : Note -> Note -> Chord -> Bool
chordWithinRange start end chord =
    case notesForChord chord of
        Just notes ->
            let
                nums =
                    List.map noteAsNumber notes

                lo =
                    List.minimum nums |> Maybe.withDefault (noteAsNumber start)

                hi =
                    List.maximum nums |> Maybe.withDefault (noteAsNumber end)
            in
            lo >= noteAsNumber start && hi < noteAsNumber end

        Nothing ->
            False


{-| Return a list of all of the pitch classes that we know about.
-}
allPitchClasses : List PitchClass
allPitchClasses =
    [ C
    , C_sharp
    , D
    , D_sharp
    , E
    , F
    , F_sharp
    , G
    , G_sharp
    , A
    , A_sharp
    , B
    ]


{-| Return a list of all of the chords that we know about.
Only create chords from the range of notes delimited by the range `start` and
`end`.
-}
allChords :
    { start : Note
    , end : Note
    , inversions : List ChordInversion
    , chordTypes : List ChordType
    , pitchClasses : List PitchClass
    }
    -> List Chord
allChords { start, end, inversions, chordTypes, pitchClasses } =
    let
        notes =
            notesFromRange start end
                |> List.filter (\note -> List.member (classifyNote note) pitchClasses)
    in
    notes
        |> List.Extra.andThen
            (\note ->
                chordTypes
                    |> List.Extra.andThen
                        (\chordType ->
                            inversions
                                |> List.Extra.andThen
                                    (\inversion ->
                                        [ { note = note
                                          , chordType = chordType
                                          , chordInversion = inversion
                                          }
                                        ]
                                    )
                        )
            )
        |> List.filter (chordWithinRange start end)


{-| Return a human-readable format of `note`.
-}
viewNote : Note -> String
viewNote note =
    note |> getNoteMetadata |> .label


{-| Return a human-readable format of `chord`.
-}
viewChord : Chord -> String
viewChord { note, chordType, chordInversion } =
    viewPitchClass (classifyNote note) ++ " " ++ chordTypeName chordType ++ " " ++ inversionName chordInversion ++ " position"


{-| Return a human-readable format of `pitchClass`.
-}
viewPitchClass : PitchClass -> String
viewPitchClass pitchClass =
    case pitchClass 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"


viewMode : Mode -> String
viewMode mode =
    case mode of
        MajorMode ->
            "major"

        MinorMode ->
            "minor"

        BluesMode ->
            "blues"


{-| Return the human-readable format of `key`.
-}
viewKey : Key -> String
viewKey { pitchClass, mode } =
    viewPitchClass pitchClass ++ " " ++ viewMode mode


{-| Returns a pairing of a scale-degree to the type of chord at that scale
degree.
-}
practiceChordsForMode : Mode -> Dict ScaleDegree ChordType
practiceChordsForMode mode =
    case mode of
        MajorMode ->
            Dict.fromList
                [ ( 1, Major )
                , ( 2, Minor )
                , ( 3, Minor )
                , ( 4, Major )
                , ( 5, Major )
                , ( 6, Minor )
                , ( 7, Diminished )
                ]

        MinorMode ->
            Dict.fromList
                [ ( 1, Minor )
                , ( 2, Diminished )
                , ( 3, Major )
                , ( 4, Minor )
                , ( 5, Minor )
                , ( 6, Major )
                , ( 7, Major )
                ]

        BluesMode ->
            Dict.fromList
                [ ( 1, MajorDominant7 )

                -- While many refer to the blues progression as a I-IV-V, the IV
                -- chord is really a MajorDominant7 made from the third scale
                -- degree.
                , ( 3, MajorDominant7 )
                , ( 5, MajorDominant7 )
                ]


{-| Returns a list of chords for a particular `key`.
-}
chordsForKey : Key -> List Chord
chordsForKey key =
    let
        chords =
            practiceChordsForMode key.mode
    in
    notesForKey key
        |> List.indexedMap
            (\i note ->
                case Dict.get (i + 1) chords of
                    Nothing ->
                        Nothing

                    Just chordType ->
                        Just
                            (allInversions
                                |> List.Extra.andThen
                                    (\inversion ->
                                        [ { note = note
                                          , chordType = chordType
                                          , chordInversion = inversion
                                          }
                                        ]
                                    )
                            )
            )
        |> Maybe.Extra.values
        |> List.concat