Properly support chord inversions
While I did change a lot of functionality, I also ran `elm-format` across the codebase, which makes these changes a bit noisy. Here is the TL;DR: - Properly support chord inversions - Ensure that the piano styling changes dynamically when I change the variables like `naturalWidth` - Add start and end notes to define the size of the piano and which chords we create - Support elm-format and run it across entire project - Debug Misc.comesBefore - Introduce a ChordInspector and debugger TODO: Ensure that we only generate chords where all of the notes can be rendered on the displayed keys. TODO: Add preferences panel, so that I can do things like "Practice blues chords in C and E with chord substitutions."
This commit is contained in:
parent
730aecc076
commit
24692ab465
9 changed files with 1531 additions and 500 deletions
3
website/sandbox/chord-drill-sergeant/dir-locals.nix
Normal file
3
website/sandbox/chord-drill-sergeant/dir-locals.nix
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
let
|
||||||
|
briefcase = import <briefcase> {};
|
||||||
|
in briefcase.utils.nixBufferFromShell ./shell.nix
|
|
@ -12,6 +12,7 @@
|
||||||
"elm/random": "1.0.0",
|
"elm/random": "1.0.0",
|
||||||
"elm/time": "1.0.0",
|
"elm/time": "1.0.0",
|
||||||
"elm-community/list-extra": "8.2.3",
|
"elm-community/list-extra": "8.2.3",
|
||||||
|
"elm-community/maybe-extra": "5.2.0",
|
||||||
"elm-community/random-extra": "3.1.0"
|
"elm-community/random-extra": "3.1.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
|
|
|
@ -3,5 +3,6 @@ let
|
||||||
in pkgs.mkShell {
|
in pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
elmPackages.elm
|
elmPackages.elm
|
||||||
|
elmPackages.elm-format
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
25
website/sandbox/chord-drill-sergeant/src/ChordInspector.elm
Normal file
25
website/sandbox/chord-drill-sergeant/src/ChordInspector.elm
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
module ChordInspector exposing (render)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Theory
|
||||||
|
|
||||||
|
|
||||||
|
render : Theory.Chord -> Html a
|
||||||
|
render chord =
|
||||||
|
case Theory.notesForChord chord of
|
||||||
|
Nothing ->
|
||||||
|
p [] [ text "Cannot retrieve the notes for the chord." ]
|
||||||
|
|
||||||
|
Just notes ->
|
||||||
|
ul []
|
||||||
|
(notes
|
||||||
|
|> List.map
|
||||||
|
(\note ->
|
||||||
|
li []
|
||||||
|
[ text
|
||||||
|
(Theory.viewNote
|
||||||
|
note
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
|
@ -1,254 +1,183 @@
|
||||||
module Main exposing (main)
|
module Main exposing (main)
|
||||||
|
|
||||||
import Browser
|
import Browser
|
||||||
|
import ChordInspector
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (..)
|
import Html.Events exposing (..)
|
||||||
|
import Piano
|
||||||
import Random
|
import Random
|
||||||
import Random.List
|
import Random.List
|
||||||
|
import Tempo
|
||||||
|
import Theory
|
||||||
import Time exposing (..)
|
import Time exposing (..)
|
||||||
|
|
||||||
import Piano
|
|
||||||
import Theory
|
|
||||||
import Tempo
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ whitelistedChords : List Theory.Chord
|
{ whitelistedChords : List Theory.Chord
|
||||||
, selectedChord : Theory.Chord
|
, selectedChord : Theory.Chord
|
||||||
, isPaused : Bool
|
, isPaused : Bool
|
||||||
, tempo : Int
|
, tempo : Int
|
||||||
|
, firstNote : Theory.Note
|
||||||
|
, lastNote : Theory.Note
|
||||||
|
, debug :
|
||||||
|
{ enable : Bool
|
||||||
|
, inspectChord : Bool
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Msg = NextChord
|
|
||||||
|
type Msg
|
||||||
|
= NextChord
|
||||||
| NewChord Theory.Chord
|
| NewChord Theory.Chord
|
||||||
| Play
|
| Play
|
||||||
| Pause
|
| Pause
|
||||||
| IncreaseTempo
|
| IncreaseTempo
|
||||||
| DecreaseTempo
|
| DecreaseTempo
|
||||||
| SetTempo String
|
| SetTempo String
|
||||||
|
| ToggleInspectChord
|
||||||
|
|
||||||
|
|
||||||
tempoStep : Int
|
tempoStep : Int
|
||||||
tempoStep = 5
|
tempoStep =
|
||||||
|
5
|
||||||
|
|
||||||
|
|
||||||
{-| Return the number of milliseconds that elapse during an interval in a
|
{-| Return the number of milliseconds that elapse during an interval in a
|
||||||
`target` bpm.
|
`target` bpm.
|
||||||
-}
|
-}
|
||||||
bpmToMilliseconds : Int -> Int
|
bpmToMilliseconds : Int -> Int
|
||||||
bpmToMilliseconds target =
|
bpmToMilliseconds target =
|
||||||
let msPerMinute = 1000 * 60
|
let
|
||||||
in round (toFloat msPerMinute / toFloat target)
|
msPerMinute =
|
||||||
|
1000 * 60
|
||||||
|
in
|
||||||
|
round (toFloat msPerMinute / toFloat target)
|
||||||
|
|
||||||
inspectChord : Theory.Chord -> String
|
|
||||||
inspectChord {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")
|
|
||||||
|
|
||||||
viewChord : Theory.Chord -> String
|
|
||||||
viewChord {note, chordType, chordPosition} =
|
|
||||||
viewNoteClass (Theory.classifyNote 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 : Theory.Note -> String
|
|
||||||
viewNote note =
|
|
||||||
case note of
|
|
||||||
Theory.C1 -> "C1"
|
|
||||||
Theory.C_sharp1 -> "C♯/D♭1"
|
|
||||||
Theory.D1 -> "D1"
|
|
||||||
Theory.D_sharp1 -> "D♯/E♭1"
|
|
||||||
Theory.E1 -> "E1"
|
|
||||||
Theory.F1 -> "F1"
|
|
||||||
Theory.F_sharp1 -> "F♯/G♭1"
|
|
||||||
Theory.G1 -> "G1"
|
|
||||||
Theory.G_sharp1 -> "G♯/A♭1"
|
|
||||||
Theory.A1 -> "A1"
|
|
||||||
Theory.A_sharp1 -> "A♯/B♭1"
|
|
||||||
Theory.B1 -> "B1"
|
|
||||||
Theory.C2 -> "C2"
|
|
||||||
Theory.C_sharp2 -> "C♯/D♭2"
|
|
||||||
Theory.D2 -> "D2"
|
|
||||||
Theory.D_sharp2 -> "D♯/E♭2"
|
|
||||||
Theory.E2 -> "E2"
|
|
||||||
Theory.F2 -> "F2"
|
|
||||||
Theory.F_sharp2 -> "F♯/G♭2"
|
|
||||||
Theory.G2 -> "G2"
|
|
||||||
Theory.G_sharp2 -> "G♯/A♭2"
|
|
||||||
Theory.A2 -> "A2"
|
|
||||||
Theory.A_sharp2 -> "A♯/B♭2"
|
|
||||||
Theory.B2 -> "B2"
|
|
||||||
Theory.C3 -> "C3"
|
|
||||||
Theory.C_sharp3 -> "C♯/D♭3"
|
|
||||||
Theory.D3 -> "D3"
|
|
||||||
Theory.D_sharp3 -> "D♯/E♭3"
|
|
||||||
Theory.E3 -> "E3"
|
|
||||||
Theory.F3 -> "F3"
|
|
||||||
Theory.F_sharp3 -> "F♯/G♭3"
|
|
||||||
Theory.G3 -> "G3"
|
|
||||||
Theory.G_sharp3 -> "G♯/A♭3"
|
|
||||||
Theory.A3 -> "A3"
|
|
||||||
Theory.A_sharp3 -> "A♯/B♭3"
|
|
||||||
Theory.B3 -> "B3"
|
|
||||||
Theory.C4 -> "C4"
|
|
||||||
Theory.C_sharp4 -> "C♯/D♭4"
|
|
||||||
Theory.D4 -> "D4"
|
|
||||||
Theory.D_sharp4 -> "D♯/E♭4"
|
|
||||||
Theory.E4 -> "E4"
|
|
||||||
Theory.F4 -> "F4"
|
|
||||||
Theory.F_sharp4 -> "F♯/G♭4"
|
|
||||||
Theory.G4 -> "G4"
|
|
||||||
Theory.G_sharp4 -> "G♯/A♭4"
|
|
||||||
Theory.A4 -> "A4"
|
|
||||||
Theory.A_sharp4 -> "A♯/B♭4"
|
|
||||||
Theory.B4 -> "B4"
|
|
||||||
Theory.C5 -> "C5"
|
|
||||||
Theory.C_sharp5 -> "C♯/D♭5"
|
|
||||||
Theory.D5 -> "D5"
|
|
||||||
Theory.D_sharp5 -> "D♯/E♭5"
|
|
||||||
Theory.E5 -> "E5"
|
|
||||||
Theory.F5 -> "F5"
|
|
||||||
Theory.F_sharp5 -> "F♯/G♭5"
|
|
||||||
Theory.G5 -> "G5"
|
|
||||||
Theory.G_sharp5 -> "G♯/A♭5"
|
|
||||||
Theory.A5 -> "A5"
|
|
||||||
Theory.A_sharp5 -> "A♯/B♭5"
|
|
||||||
Theory.B5 -> "B5"
|
|
||||||
Theory.C6 -> "C6"
|
|
||||||
Theory.C_sharp6 -> "C♯/D♭6"
|
|
||||||
Theory.D6 -> "D6"
|
|
||||||
Theory.D_sharp6 -> "D♯/E♭6"
|
|
||||||
Theory.E6 -> "E6"
|
|
||||||
Theory.F6 -> "F6"
|
|
||||||
Theory.F_sharp6 -> "F♯/G♭6"
|
|
||||||
Theory.G6 -> "G6"
|
|
||||||
Theory.G_sharp6 -> "G♯/A♭6"
|
|
||||||
Theory.A6 -> "A6"
|
|
||||||
Theory.A_sharp6 -> "A♯/B♭6"
|
|
||||||
Theory.B6 -> "B6"
|
|
||||||
Theory.C7 -> "C7"
|
|
||||||
Theory.C_sharp7 -> "C♯/D♭7"
|
|
||||||
Theory.D7 -> "D7"
|
|
||||||
Theory.D_sharp7 -> "D♯/E♭7"
|
|
||||||
Theory.E7 -> "E7"
|
|
||||||
Theory.F7 -> "F7"
|
|
||||||
Theory.F_sharp7 -> "F♯/G♭7"
|
|
||||||
Theory.G7 -> "G7"
|
|
||||||
Theory.G_sharp7 -> "G♯/A♭7"
|
|
||||||
Theory.A7 -> "A7"
|
|
||||||
Theory.A_sharp7 -> "A♯/B♭7"
|
|
||||||
Theory.B7 -> "B7"
|
|
||||||
Theory.C8 -> "C8"
|
|
||||||
|
|
||||||
{-| Serialize a human-readable format of `noteClass`. -}
|
|
||||||
viewNoteClass : Theory.NoteClass -> String
|
|
||||||
viewNoteClass noteClass =
|
|
||||||
case noteClass of
|
|
||||||
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
|
||||||
cmajor =
|
cmajor =
|
||||||
{ note = Theory.C4
|
{ note = Theory.C4
|
||||||
, chordType = Theory.Major
|
, chordType = Theory.MajorDominant7
|
||||||
, chordPosition = Theory.First
|
, chordInversion = Theory.Root
|
||||||
}
|
}
|
||||||
|
|
||||||
{-| The initial state for the application. -}
|
|
||||||
|
{-| The initial state for the application.
|
||||||
|
-}
|
||||||
init : Model
|
init : Model
|
||||||
init =
|
init =
|
||||||
{ whitelistedChords = Theory.allChords
|
let
|
||||||
|
( firstNote, lastNote ) =
|
||||||
|
( Theory.C3, Theory.C5 )
|
||||||
|
in
|
||||||
|
{ whitelistedChords = Theory.allChords firstNote lastNote
|
||||||
, selectedChord = cmajor
|
, selectedChord = cmajor
|
||||||
, isPaused = True
|
, isPaused = True
|
||||||
, tempo = 60
|
, tempo = 60
|
||||||
|
, firstNote = firstNote
|
||||||
|
, lastNote = lastNote
|
||||||
|
, debug =
|
||||||
|
{ enable = True
|
||||||
|
, inspectChord = True
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions { isPaused, tempo } =
|
subscriptions { isPaused, tempo } =
|
||||||
if isPaused then
|
if isPaused then
|
||||||
Sub.none
|
Sub.none
|
||||||
|
|
||||||
else
|
else
|
||||||
Time.every (tempo |> bpmToMilliseconds |> toFloat) (\_ -> NextChord)
|
Time.every (tempo |> bpmToMilliseconds |> toFloat) (\_ -> NextChord)
|
||||||
|
|
||||||
{-| Now that we have state, we need a function to change the state. -}
|
|
||||||
|
{-| Now that we have state, we need a function to change the state.
|
||||||
|
-}
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
NewChord chord -> ( { model | selectedChord = chord }
|
NewChord chord ->
|
||||||
|
( { model | selectedChord = chord }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
NextChord -> ( model
|
|
||||||
, Random.generate (\x ->
|
NextChord ->
|
||||||
|
( model
|
||||||
|
, Random.generate
|
||||||
|
(\x ->
|
||||||
case x of
|
case x of
|
||||||
(Just chord, _) -> NewChord chord
|
( Just chord, _ ) ->
|
||||||
(Nothing, _) -> NewChord cmajor)
|
NewChord chord
|
||||||
|
|
||||||
|
( Nothing, _ ) ->
|
||||||
|
NewChord cmajor
|
||||||
|
)
|
||||||
(Random.List.choose model.whitelistedChords)
|
(Random.List.choose model.whitelistedChords)
|
||||||
)
|
)
|
||||||
Play -> ( { model | isPaused = False }
|
|
||||||
|
Play ->
|
||||||
|
( { model | isPaused = False }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
Pause -> ( { model | isPaused = True }
|
|
||||||
|
Pause ->
|
||||||
|
( { model | isPaused = True }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
IncreaseTempo -> ( { model | tempo = model.tempo + tempoStep }
|
|
||||||
|
IncreaseTempo ->
|
||||||
|
( { model | tempo = model.tempo + tempoStep }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
DecreaseTempo -> ( { model | tempo = model.tempo - tempoStep }
|
|
||||||
|
DecreaseTempo ->
|
||||||
|
( { model | tempo = model.tempo - tempoStep }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
SetTempo tempo -> ( { model |
|
|
||||||
tempo = case String.toInt tempo of
|
ToggleInspectChord ->
|
||||||
Just x -> x
|
( { model
|
||||||
Nothing -> model.tempo
|
| debug =
|
||||||
|
{ inspectChord = not model.debug.inspectChord
|
||||||
|
, enable = model.debug.enable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SetTempo tempo ->
|
||||||
|
( { model
|
||||||
|
| tempo =
|
||||||
|
case String.toInt tempo of
|
||||||
|
Just x ->
|
||||||
|
x
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
model.tempo
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
playPause : Model -> Html Msg
|
playPause : Model -> Html Msg
|
||||||
playPause { isPaused } =
|
playPause { isPaused } =
|
||||||
if isPaused then
|
if isPaused then
|
||||||
button [ onClick Play ] [ text "Play" ]
|
button [ onClick Play ] [ text "Play" ]
|
||||||
|
|
||||||
else
|
else
|
||||||
button [ onClick Pause ] [ text "Pause" ]
|
button [ onClick Pause ] [ text "Pause" ]
|
||||||
|
|
||||||
|
|
||||||
|
debugger : Html Msg
|
||||||
|
debugger =
|
||||||
|
fieldset []
|
||||||
|
[ label [] [ text "Inspect Chord" ]
|
||||||
|
, input [ type_ "checkbox", onClick ToggleInspectChord, checked init.debug.inspectChord ] []
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Msg
|
view : Model -> Html Msg
|
||||||
view model =
|
view model =
|
||||||
case Theory.notesForChord model.selectedChord of
|
case Theory.notesForChord model.selectedChord of
|
||||||
|
@ -259,21 +188,41 @@ view model =
|
||||||
or lower end of the piano.
|
or lower end of the piano.
|
||||||
|
|
||||||
Chord:
|
Chord:
|
||||||
""" ++ (inspectChord model.selectedChord)) ]
|
""" ++ Theory.inspectChord model.selectedChord) ]
|
||||||
|
|
||||||
Just x ->
|
Just x ->
|
||||||
div [] [ Tempo.render { tempo = model.tempo
|
div []
|
||||||
|
[ Tempo.render
|
||||||
|
{ tempo = model.tempo
|
||||||
, handleIncrease = IncreaseTempo
|
, handleIncrease = IncreaseTempo
|
||||||
, handleDecrease = DecreaseTempo
|
, handleDecrease = DecreaseTempo
|
||||||
, handleInput = SetTempo
|
, handleInput = SetTempo
|
||||||
}
|
}
|
||||||
, playPause model
|
, playPause model
|
||||||
, p [] [ text (viewChord model.selectedChord) ]
|
, if model.debug.enable then
|
||||||
, Piano.render { highlight = x }
|
debugger
|
||||||
|
|
||||||
|
else
|
||||||
|
span [] []
|
||||||
|
, if model.debug.inspectChord then
|
||||||
|
ChordInspector.render model.selectedChord
|
||||||
|
|
||||||
|
else
|
||||||
|
span [] []
|
||||||
|
, p [] [ text (Theory.viewChord model.selectedChord) ]
|
||||||
|
, Piano.render
|
||||||
|
{ highlight = x
|
||||||
|
, start = model.firstNote
|
||||||
|
, end = model.lastNote
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
{-| For now, I'm just dumping things onto the page to sketch ideas. -}
|
|
||||||
|
{-| For now, I'm just dumping things onto the page to sketch ideas.
|
||||||
|
-}
|
||||||
main =
|
main =
|
||||||
Browser.element { init = \() -> (init, Cmd.none)
|
Browser.element
|
||||||
|
{ init = \() -> ( init, Cmd.none )
|
||||||
, subscriptions = subscriptions
|
, subscriptions = subscriptions
|
||||||
, update = update
|
, update = update
|
||||||
, view = view
|
, view = view
|
||||||
|
|
|
@ -1,15 +1,35 @@
|
||||||
module Misc exposing (..)
|
module Misc exposing (..)
|
||||||
|
|
||||||
|
|
||||||
comesAfter : a -> List a -> Maybe a
|
comesAfter : a -> List a -> Maybe a
|
||||||
comesAfter x xs =
|
comesAfter x xs =
|
||||||
case xs of
|
case xs of
|
||||||
[] -> Nothing
|
[] ->
|
||||||
_::[] -> Nothing
|
Nothing
|
||||||
y::z::rest -> if y == x then Just z else comesAfter x (z::rest)
|
|
||||||
|
_ :: [] ->
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
y :: z :: rest ->
|
||||||
|
if y == x then
|
||||||
|
Just z
|
||||||
|
|
||||||
|
else
|
||||||
|
comesAfter x (z :: rest)
|
||||||
|
|
||||||
|
|
||||||
comesBefore : a -> List a -> Maybe a
|
comesBefore : a -> List a -> Maybe a
|
||||||
comesBefore x xs =
|
comesBefore x xs =
|
||||||
case xs of
|
case xs of
|
||||||
[] -> Nothing
|
[] ->
|
||||||
_::[] -> Nothing
|
Nothing
|
||||||
y::z::rest -> if z == x then Just y else comesAfter x (z::rest)
|
|
||||||
|
_ :: [] ->
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
y :: z :: rest ->
|
||||||
|
if z == x then
|
||||||
|
Just y
|
||||||
|
|
||||||
|
else
|
||||||
|
comesBefore x (z :: rest)
|
||||||
|
|
|
@ -4,46 +4,95 @@ import Browser
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (..)
|
import Html.Events exposing (..)
|
||||||
|
import List.Extra
|
||||||
import Theory
|
import Theory
|
||||||
|
|
||||||
{-| Convert an integer into its pixel representation for CSS. -}
|
|
||||||
|
type alias KeyMarkup a =
|
||||||
|
{ offset : Int
|
||||||
|
, isHighlit : Bool
|
||||||
|
, note : Theory.Note
|
||||||
|
}
|
||||||
|
-> Html a
|
||||||
|
|
||||||
|
|
||||||
|
type alias Props =
|
||||||
|
{ highlight : List Theory.Note
|
||||||
|
, start : Theory.Note
|
||||||
|
, end : Theory.Note
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Convert an integer into its pixel representation for CSS.
|
||||||
|
-}
|
||||||
pixelate : Int -> String
|
pixelate : Int -> String
|
||||||
pixelate x = String.fromInt x ++ "px"
|
pixelate x =
|
||||||
|
String.fromInt x ++ "px"
|
||||||
|
|
||||||
{-| Pixel width of the white keys. -}
|
|
||||||
|
{-| Pixel width of the white keys.
|
||||||
|
-}
|
||||||
naturalWidth : Int
|
naturalWidth : Int
|
||||||
naturalWidth = 40
|
naturalWidth =
|
||||||
|
45
|
||||||
|
|
||||||
{-| Pixel height of the white keys. -}
|
|
||||||
|
{-| Pixel height of the white keys.
|
||||||
|
-}
|
||||||
naturalHeight : Int
|
naturalHeight : Int
|
||||||
naturalHeight = 200
|
naturalHeight =
|
||||||
|
250
|
||||||
|
|
||||||
{-| Pixel width of the black keys. -}
|
|
||||||
|
{-| Pixel width of the black keys.
|
||||||
|
-}
|
||||||
accidentalWidth : Int
|
accidentalWidth : Int
|
||||||
accidentalWidth = round (toFloat naturalWidth * 0.7)
|
accidentalWidth =
|
||||||
|
round (toFloat naturalWidth * 0.4)
|
||||||
|
|
||||||
{-| Pixel height of the black keys. -}
|
|
||||||
|
{-| Pixel height of the black keys.
|
||||||
|
-}
|
||||||
accidentalHeight : Int
|
accidentalHeight : Int
|
||||||
accidentalHeight = round (toFloat naturalHeight * 0.6)
|
accidentalHeight =
|
||||||
|
round (toFloat naturalHeight * 0.63)
|
||||||
|
|
||||||
{-| These are the white keys on most modern pianos. -}
|
|
||||||
natural : Int -> Bool -> Html a
|
{-| These are the white keys on most modern pianos.
|
||||||
natural offset isHighlit =
|
-}
|
||||||
div [ style "background-color" (if isHighlit then "red" else "white")
|
natural : KeyMarkup a
|
||||||
|
natural { offset, isHighlit, note } =
|
||||||
|
div
|
||||||
|
[ style "background-color"
|
||||||
|
(if isHighlit then
|
||||||
|
"red"
|
||||||
|
|
||||||
|
else
|
||||||
|
"white"
|
||||||
|
)
|
||||||
, style "border-right" "1px solid black"
|
, style "border-right" "1px solid black"
|
||||||
, style "border-top" "1px solid black"
|
, style "border-top" "1px solid black"
|
||||||
, style "border-bottom" "1px solid black"
|
, style "border-bottom" "1px solid black"
|
||||||
, style "width" (pixelate naturalWidth)
|
, style "width" (pixelate naturalWidth)
|
||||||
, style "height" (pixelate naturalHeight)
|
, style "height" (pixelate naturalHeight)
|
||||||
, style "position" "absolute"
|
, style "position" "absolute"
|
||||||
, style "left" ((String.fromInt offset) ++ "px")
|
, style "left" (String.fromInt offset ++ "px")
|
||||||
] []
|
]
|
||||||
|
[ p [] [ text (Theory.viewNote note) ] ]
|
||||||
|
|
||||||
{-| These are the black keys on most modern pianos. -}
|
|
||||||
accidental : Int -> Bool -> Html a
|
{-| These are the black keys on most modern pianos.
|
||||||
accidental offset isHighlit =
|
-}
|
||||||
div [ style "background-color" (if isHighlit then "red" else "black")
|
accidental : KeyMarkup a
|
||||||
|
accidental { offset, isHighlit, note } =
|
||||||
|
div
|
||||||
|
[ style "background-color"
|
||||||
|
(if isHighlit then
|
||||||
|
"red"
|
||||||
|
|
||||||
|
else
|
||||||
|
"black"
|
||||||
|
)
|
||||||
, style "border-top" "1px solid black"
|
, style "border-top" "1px solid black"
|
||||||
, style "border-left" "1px solid black"
|
, style "border-left" "1px solid black"
|
||||||
, style "border-right" "1px solid black"
|
, style "border-right" "1px solid black"
|
||||||
|
@ -51,32 +100,88 @@ accidental offset isHighlit =
|
||||||
, style "width" (pixelate accidentalWidth)
|
, style "width" (pixelate accidentalWidth)
|
||||||
, style "height" (pixelate accidentalHeight)
|
, style "height" (pixelate accidentalHeight)
|
||||||
, style "position" "absolute"
|
, style "position" "absolute"
|
||||||
, style "left" ((String.fromInt offset) ++ "px")
|
, style "left" (String.fromInt offset ++ "px")
|
||||||
, style "z-index" "1"
|
, style "z-index" "1"
|
||||||
] []
|
]
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
makeKey : List Theory.Note -> Theory.Note -> (Int -> Html a)
|
||||||
|
makeKey highlight note =
|
||||||
|
if Theory.isNatural note then
|
||||||
|
\x ->
|
||||||
|
natural
|
||||||
|
{ offset = x
|
||||||
|
, isHighlit = List.member note highlight
|
||||||
|
, note = note
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
\x ->
|
||||||
|
accidental
|
||||||
|
{ offset = x
|
||||||
|
, isHighlit = List.member note highlight
|
||||||
|
, note = note
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| A section of the piano consisting of all twelve notes. The name octave
|
{-| A section of the piano consisting of all twelve notes. The name octave
|
||||||
implies eight notes, which most scales (not the blues scale) honor. -}
|
implies eight notes, which most scales (not the blues scale) honor.
|
||||||
octave : List Theory.Note -> List (Html a)
|
-}
|
||||||
octave highlight =
|
octave : Theory.Note -> Theory.Note -> List Theory.Note -> List (Html a)
|
||||||
|
octave start end highlight =
|
||||||
let
|
let
|
||||||
isHighlit note = List.member note highlight
|
isHighlit note =
|
||||||
in
|
List.member note highlight
|
||||||
[ natural 0 (isHighlit Theory.C4)
|
|
||||||
, accidental 25 (isHighlit Theory.C_sharp4)
|
|
||||||
, natural 40 (isHighlit Theory.D4)
|
|
||||||
, accidental 65 (isHighlit Theory.D_sharp4)
|
|
||||||
, natural 80 (isHighlit Theory.E4)
|
|
||||||
, natural 120 (isHighlit Theory.F4)
|
|
||||||
, accidental 145 (isHighlit Theory.F_sharp4)
|
|
||||||
, natural 160 (isHighlit Theory.G4)
|
|
||||||
, accidental 185 (isHighlit Theory.G_sharp4)
|
|
||||||
, natural 200 (isHighlit Theory.A4)
|
|
||||||
, accidental 225 (isHighlit Theory.A_sharp4)
|
|
||||||
, natural 240 (isHighlit Theory.B4)
|
|
||||||
]
|
|
||||||
|
|
||||||
{-| Return the HTML that renders a piano representation. -}
|
spacing prevOffset prev curr =
|
||||||
render : { highlight : List Theory.Note } -> Html a
|
case ( Theory.keyClass prev, Theory.keyClass curr ) of
|
||||||
render {highlight} =
|
( Theory.Natural, Theory.Accidental ) ->
|
||||||
div [ style "display" "flex" ] (octave highlight |> List.reverse |> List.repeat 1 |> List.concat)
|
-- idk this calculation yet
|
||||||
|
prevOffset + naturalWidth - round (toFloat accidentalWidth / 2)
|
||||||
|
|
||||||
|
( Theory.Accidental, Theory.Natural ) ->
|
||||||
|
-- accidentalWidth / 2
|
||||||
|
prevOffset + round (toFloat accidentalWidth / 2)
|
||||||
|
|
||||||
|
( Theory.Natural, Theory.Natural ) ->
|
||||||
|
-- naturalWidth
|
||||||
|
prevOffset + naturalWidth
|
||||||
|
|
||||||
|
-- This pattern should never hit.
|
||||||
|
_ ->
|
||||||
|
prevOffset
|
||||||
|
|
||||||
|
( _, _, notes ) =
|
||||||
|
Theory.notesFromRange start end
|
||||||
|
|> List.foldl
|
||||||
|
(\curr ( prevOffset, prev, result ) ->
|
||||||
|
case ( prevOffset, prev ) of
|
||||||
|
( Nothing, Nothing ) ->
|
||||||
|
( Just 0, Just curr, makeKey highlight curr 0 :: result )
|
||||||
|
|
||||||
|
( Just po, Just p ) ->
|
||||||
|
let
|
||||||
|
offset =
|
||||||
|
spacing po p curr
|
||||||
|
in
|
||||||
|
( Just offset
|
||||||
|
, Just curr
|
||||||
|
, makeKey highlight curr offset :: result
|
||||||
|
)
|
||||||
|
|
||||||
|
-- This pattern should never hit.
|
||||||
|
_ ->
|
||||||
|
( Nothing, Nothing, [] )
|
||||||
|
)
|
||||||
|
( Nothing, Nothing, [] )
|
||||||
|
in
|
||||||
|
List.reverse notes
|
||||||
|
|
||||||
|
|
||||||
|
{-| Return the HTML that renders a piano representation.
|
||||||
|
-}
|
||||||
|
render : Props -> Html a
|
||||||
|
render { highlight, start, end } =
|
||||||
|
div [ style "display" "flex" ]
|
||||||
|
(octave start end highlight |> List.reverse |> List.repeat 1 |> List.concat)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (..)
|
import Html.Events exposing (..)
|
||||||
|
|
||||||
|
|
||||||
type alias Props msg =
|
type alias Props msg =
|
||||||
{ tempo : Int
|
{ tempo : Int
|
||||||
, handleIncrease : msg
|
, handleIncrease : msg
|
||||||
|
@ -11,12 +12,16 @@ type alias Props msg =
|
||||||
, handleInput : String -> msg
|
, handleInput : String -> msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render : Props msg -> Html msg
|
render : Props msg -> Html msg
|
||||||
render { tempo, handleIncrease, handleDecrease, handleInput } =
|
render { tempo, handleIncrease, handleDecrease, handleInput } =
|
||||||
div [] [ p [] [ text ((String.fromInt tempo) ++ " BPM") ]
|
div []
|
||||||
|
[ p [] [ text (String.fromInt tempo ++ " BPM") ]
|
||||||
, button [ onClick handleDecrease ] [ text "Slower" ]
|
, button [ onClick handleDecrease ] [ text "Slower" ]
|
||||||
, input [ onInput handleInput
|
, input
|
||||||
|
[ onInput handleInput
|
||||||
, placeholder "Set tempo..."
|
, placeholder "Set tempo..."
|
||||||
] []
|
]
|
||||||
|
[]
|
||||||
, button [ onClick handleIncrease ] [ text "Faster" ]
|
, button [ onClick handleIncrease ] [ text "Faster" ]
|
||||||
]
|
]
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue