Aoide-2.0.0.0: Composition/Keyboard.hs
{-|
Keyboard playability and reductions.
-}
module Composition.Keyboard (
Keyboard (..),
Playable (..),
keyboard_playable,
notes_to_keyboard,
parse_playables,
reduction) where
import Composition.Errors
import Composition.Notation
import Composition.Notes
import Composition.Score
import Composition.Theory
import Control.Lens.Combinators hiding (parts)
import Control.Monad.Except
import Control.Monad.Trans.Except
import Data.Char
import Data.Foldable as Foldable
import Data.Functor
import Data.List as List
import Data.Map.Strict as Map hiding (keys)
import Data.Maybe as Maybe
import Data.Ord
import Data.Set as Set
import Parser.Files
import Parser.Locations
import Parser.Parser
import Parser.Utilities
data Char_class = Delimiter_char Token | Invalid_char | Nonzero_nat_char Char | Whitespace_char | Zero_char
-- | Data structure for storing the left and right hand part of a chord or score part.
data Keyboard t = Keyboard t t
data Notes_in = Notes_in (Set (Note Pitched)) (Set (Note Pitched))
data Notes_in' = Notes_in' (Set (Note Pitched)) | Tie_in' (Set (Note Pitched))
data Notes_out = Rest_out | Notes_out (Set (Note Pitched)) | Tie_out
type Parser = Parser' Token Error
-- | Data structure for describing playable note combinations. For example, [15] [3 10] means that two notes with an interval
-- of 15 semitones are playable with the right hand only starting from D#/Eb and A#/Bb.
data Playable = Playable {semitones :: [Semitones], starting_keys :: Set Int}
data Token = Left_square_bracket_token | Newline_token | Positive_int_token Int | Right_square_bracket_token | Zero_token
type Tokeniser = Tokeniser' Char_class Token Error
instance Applicative Keyboard where
Keyboard f g <*> Keyboard left_hand right_hand = Keyboard (f left_hand) (g right_hand)
pure x = Keyboard x x
deriving instance Eq Char_class
deriving instance Eq Token
instance Foldable Keyboard where
foldr f x (Keyboard left_hand right_hand) = f left_hand (f right_hand x)
instance Functor Keyboard where
fmap f (Keyboard left_hand right_hand) = Keyboard (f left_hand) (f right_hand)
instance Traversable Keyboard where
traverse f (Keyboard left_hand right_hand) = Keyboard <$> f left_hand <*> f right_hand
all_notes :: [Playable] -> [Note Pitched] -> [Set (Note Pitched)]
all_notes playables notes =
Set.empty : all_notes' Set.empty notes where
all_notes' :: Set (Note Pitched) -> [Note Pitched] -> [Set (Note Pitched)]
all_notes' picked_notes notes' =
case notes' of
[] -> []
note : notes'' ->
let
picked_notes' = Set.insert note picked_notes in
case notes_playable playables picked_notes' of
False -> all_notes' picked_notes notes''
True -> picked_notes' : all_notes' picked_notes' notes''
best :: (Eq u) => (t -> u) -> ([u] -> u) -> [t] -> [t]
best metric best_metric x = List.filter (\ y -> best_metric (metric <$> x) == metric y) x
best_keyboard :: [Keyboard (Set (Note Pitched))] -> Keyboard (Set (Note Pitched))
best_keyboard =
(
head <$>
best right_hand_note_count maximum <$>
best narrower_hand_span minimum <$>
best wider_hand_note_count minimum <$>
best wider_hand_span minimum <$>
best crosses_middle_c minimum <$>
best note_count maximum)
classify_char :: Char -> Char_class
classify_char c =
case c of
'\n' -> Delimiter_char Newline_token
' ' -> Whitespace_char
'0' -> Zero_char
_ | isDigit c && c /= '0' -> Nonzero_nat_char c
'[' -> Delimiter_char Left_square_bracket_token
']' -> Delimiter_char Right_square_bracket_token
_ -> Invalid_char
construct_keys :: [Int] -> Either (Location -> Error) (Set Int)
construct_keys keys =
case construct_set keys of
Nothing -> Left Duplicate_keys
Just keys' -> Right keys'
continuing_notes_and_event ::
Set (Note Pitched) -> Length_fraction -> Set (Note Pitched) -> Set (Note Pitched) -> (Set (Note Pitched), Event' Notes_out)
continuing_notes_and_event ties len old_notes new_notes =
let
(continuing_notes, notes_out) =
case Set.elems new_notes of
[] ->
case Set.elems (Set.union old_notes ties) of
[] -> (Set.empty, Rest_out)
_ : _ -> (old_notes, Tie_out)
_ : _ -> (new_notes, Notes_out new_notes) in
(continuing_notes, Event' notes_out len)
crosses_middle_c :: Keyboard (Set (Note Pitched)) -> Int
crosses_middle_c (Keyboard left_hand_notes right_hand_notes) =
crosses_middle_c' id Set.findMax left_hand_notes `max` crosses_middle_c' flip Set.findMin right_hand_notes
crosses_middle_c' ::
(
((Note Pitched -> Note Pitched -> Int) -> Note Pitched -> Note Pitched -> Int) ->
(Set (Note Pitched) -> Note Pitched) ->
Set (Note Pitched) ->
Int)
crosses_middle_c' order_arguments innermost_note notes =
order_arguments distance_in_semitones (Pitched_note 4 C) (innermost_note (Set.insert (Pitched_note 4 C) notes))
delimiter_char :: Char_class -> Maybe Token
delimiter_char char_class =
case char_class of
Delimiter_char token -> Just token
_ -> Nothing
events_out_to_events :: [Event' Notes_out] -> [Event_fraction Pitched]
events_out_to_events events =
case events of
[] -> []
Event' notes len : events' -> events_out_to_events' (Event' (notes_out_to_set notes) len) events'
events_out_to_events' :: Event_fraction Pitched -> [Event' Notes_out] -> [Event_fraction Pitched]
events_out_to_events' event@(Event' notes_0 len_0) events =
case events of
[] -> [event]
Event' maybe_notes_1 len_1 : events' ->
case maybe_notes_1 of
Rest_out ->
case Set.elems notes_0 of
[] -> events_out_to_events' (Event' Set.empty (len_0 + len_1)) events'
_ : _ -> event : events_out_to_events' (Event' Set.empty len_1) events'
Notes_out notes_1 -> event : events_out_to_events' (Event' notes_1 len_1) events'
Tie_out -> events_out_to_events' (Event' notes_0 (len_0 + len_1)) events'
events_to_events_in :: [[Event' (Set (Note Pitched))]] -> [Event' Notes_in]
events_to_events_in events = events_to_events_in' ((<$>) (over #event_notes Notes_in') <$> events)
events_to_events_in' :: [[Event' (Notes_in')]] -> [Event' Notes_in]
events_to_events_in' events =
case () of
() | all Foldable.null events -> []
() ->
let
len = minimum (view #event_length <$> head <$> events)
heads_and_tails = head_and_tail len <$> events in
(
Event' (Foldable.foldr union_notes_in (Notes_in Set.empty Set.empty) (fst <$> heads_and_tails)) len :
events_to_events_in' (snd <$> heads_and_tails))
hand_span :: Set (Note Pitched) -> Int
hand_span notes =
case Set.elems notes of
[] -> -1
_ -> distance_in_semitones (minimum notes) (maximum notes)
hands_not_crossed :: Keyboard (Set (Note Pitched)) -> Bool
hands_not_crossed (Keyboard left_hand_notes right_hand_notes) =
case (Set.elems left_hand_notes, Set.elems right_hand_notes) of
(_ : _, _ : _) -> Set.findMax left_hand_notes < Set.findMin right_hand_notes
_ -> True
head_and_tail :: Length_fraction -> [Event' (Notes_in')] -> (Notes_in, [Event' (Notes_in')])
head_and_tail len events =
case events of
[] -> undefined
Event' notes len' : events' ->
(
transform_notes_in notes,
case () of
() | len == len' -> events'
() -> Event' (notes_in_to_tie notes) (len' - len) : events')
-- | Check if this configuration is playable on keyboard.
keyboard_playable :: [Playable] -> Keyboard (Set (Note Pitched)) -> Bool
keyboard_playable playables keyboard@(Keyboard left_hand_notes right_hand_notes) =
(
notes_playable (mirror_playables playables) left_hand_notes &&
hands_not_crossed keyboard &&
notes_playable playables right_hand_notes)
mirror_key :: Int -> Int
mirror_key key = mod (4 - key) 12
mirror_playable :: Playable -> Playable
mirror_playable (Playable semitones keys) = Playable (reverse semitones) (Set.fromList (mirror_key <$> Set.elems keys))
mirror_playables :: [Playable] -> [Playable]
mirror_playables = (<$>) mirror_playable
nat_char :: Char_class -> Maybe Char
nat_char char_class =
case char_class of
Zero_char -> Just '0'
Nonzero_nat_char c -> Just c
_ -> Nothing
narrower_hand_span :: Keyboard (Set (Note Pitched)) -> Int
narrower_hand_span keyboard = minimum (hand_span <$> keyboard)
next_location :: Char_class -> Location -> Location
next_location char_class =
case char_class of
Delimiter_char Newline_token -> next_line
_ -> next_char
nonzero_nat_char :: Char_class -> Maybe Char
nonzero_nat_char char_class =
case char_class of
Nonzero_nat_char c -> Just c
_ -> Nothing
note_count :: Keyboard (Set (Note Pitched)) -> Int
note_count keyboard = sum (Set.size <$> keyboard)
notes_in_to_tie :: Notes_in' -> Notes_in'
notes_in_to_tie maybe_notes =
case maybe_notes of
Notes_in' notes -> Tie_in' notes
Tie_in' notes -> Tie_in' notes
notes_out_to_set :: Notes_out -> Set (Note Pitched)
notes_out_to_set maybe_notes =
case maybe_notes of
Rest_out -> Set.empty
Notes_out notes -> notes
Tie_out -> Set.empty
notes_playable :: [Playable] -> Set (Note Pitched) -> Bool
notes_playable playables notes =
case Set.elems notes of
[] -> True
note@(Pitched_note _ note_name) : notes' ->
any (notes_playable' (notes_to_semitones note notes') (semitones_from_c note_name)) playables
notes_playable' :: [Int] -> Int -> Playable -> Bool
notes_playable' semitones' starting_key (Playable semitones starting_keys) =
semitones == semitones' && any ((==) starting_key) starting_keys
notes_to_semitones :: Note Pitched -> [Note Pitched] -> [Int]
notes_to_semitones note_0 notes =
case notes of
[] -> []
note_1 : notes' -> distance_in_semitones note_0 note_1 : notes_to_semitones note_1 notes'
-- | Check if the set of notes is playable on keyboard and distribute the notes between hands as well as possible.
notes_to_keyboard :: [Playable] -> Set (Note Pitched) -> Maybe (Keyboard (Set (Note Pitched)))
notes_to_keyboard playables notes =
case List.filter (keyboard_playable playables) (split_notes_between_hands notes) of
[] -> Nothing
keyboards -> Just (best_keyboard keyboards)
parse_key :: Parser Int
parse_key = filter_parser ((>=) 11) Key_is_out_of_range parse_nat
parse_keys :: Parser (Set Int)
parse_keys = fmap_filter_parser construct_keys (parse_list' parse_key)
parse_list' :: Parser t -> Parser [t]
parse_list' parse_t = parse_square_brackets (parse_many parse_t)
parse_nat :: Parser Int
parse_nat = parse_zero <+> parse_positive_int
parse_playable :: Parser Playable
parse_playable = Playable <$> parse_list' parse_positive_int <*> parse_keys
-- | Parse keyboard playability file.
parse_playables :: File_path -> ExceptT Error IO [Playable]
parse_playables file_path =
do
playables <- read_file "key" readFile File_error file_path
except (fromJust (parse' classify_char next_location tokenise parse_playables' Parse_error_playables playables))
parse_playables' :: Parser [Playable]
parse_playables' = parse_list Newline_token parse_playable
parse_positive_int :: Parser Int
parse_positive_int = parse_token' positive_int_token
parse_square_brackets :: Parser t -> Parser t
parse_square_brackets = parse_brackets Left_square_bracket_token Right_square_bracket_token
parse_zero :: Parser Int
parse_zero =
do
parse_token Zero_token
return 0
pitched_events :: Pitched_or_unpitched Track -> Maybe [Event_fraction Pitched]
pitched_events = pitched_or_unpitched (\ (Track {events}) -> Just events) (\ _ -> Nothing)
positive_int_token :: Token -> Maybe Int
positive_int_token token =
case token of
Positive_int_token i -> Just i
_ -> Nothing
-- | Keyboard reduction. You have to provide playability data, optional instrument score header field and a MIDI instrument
-- code.
reduction :: [Playable] -> Map Header_field String -> MIDI_instrument -> Score -> Either Error Score
reduction playables header midi_instrument (Score {parts}) =
do
keyboard_parts <- traverse keyboard_part parts
Right (Score {header, parts = keyboard_parts}) where
all_keyboards :: Set (Note Pitched) -> [Keyboard (Set (Note Pitched))]
all_keyboards notes =
List.filter
hands_not_crossed
(
Keyboard <$>
all_notes (mirror_playables playables) (Set.elems notes) <*>
all_notes playables (reverse (Set.elems notes)))
events_to_keyboard :: Keyboard (Set (Note Pitched)) -> [Event' Notes_in] -> Keyboard [Event' Notes_out]
events_to_keyboard old_keyboard events =
case events of
[] -> pure []
Event' (Notes_in new_notes ties) len : events' ->
let
new_keyboard = best_keyboard (all_keyboards new_notes)
continuing_notes_and_event_keyboard = continuing_notes_and_event ties len <$> old_keyboard <*> new_keyboard in
(
(:) <$>
(snd <$> continuing_notes_and_event_keyboard) <*>
events_to_keyboard (fst <$> continuing_notes_and_event_keyboard) events')
keyboard_part :: Part -> Either Error Part
keyboard_part (Part {title = part_title, key, time, initial_position, tempo, stave_groups}) =
do
let tracks = remove_notational_information stave_groups
_ <- tracks_length tracks
Keyboard left_hand_track right_hand_track <-
traverse
(add_notational_information time initial_position)
(
events_out_to_events <$>
events_to_keyboard (pure Set.empty) (events_to_events_in (Maybe.mapMaybe pitched_events tracks)))
Right
(Part {
title = part_title,
key,
time,
initial_position,
tempo,
stave_groups =
[
[
Pitched
(Instrument_and_staves {
instrument_name = "",
short_instrument_name = "",
midi_instrument,
velocity = max_velocity,
staves =
[
Stave {
clef = Pitched_clef {clef_name = Treble, transposition = 0},
tracks = One_track right_hand_track},
Stave {
clef = Pitched_clef {clef_name = Bass, transposition = 0},
tracks = One_track left_hand_track}]})]]})
right_hand_note_count :: Keyboard (Set (Note Pitched)) -> Int
right_hand_note_count (Keyboard _ right_hand_notes) = Set.size right_hand_notes
split_notes_between_hands :: Set (Note Pitched) -> [Keyboard (Set (Note Pitched))]
split_notes_between_hands notes = Keyboard Set.empty notes : split_notes_between_hands' Set.empty (Set.elems notes)
split_notes_between_hands' :: Set (Note Pitched) -> [Note Pitched] -> [Keyboard (Set (Note Pitched))]
split_notes_between_hands' left_hand_notes notes =
case notes of
[] -> []
note : notes' ->
let
left_hand_notes' = Set.insert note left_hand_notes in
Keyboard left_hand_notes' (Set.fromList notes') : split_notes_between_hands' left_hand_notes' notes'
tokenise :: Tokeniser ()
tokenise = void (parse_many tokenise_1)
tokenise_1 :: Tokeniser ()
tokenise_1 = tokenise_delimiter <+> tokenise_nat <+> tokenise_whitespace
tokenise_delimiter :: Tokeniser ()
tokenise_delimiter = add_token (parse_token' delimiter_char)
tokenise_nat :: Tokeniser ()
tokenise_nat = add_token (tokenise_zero <+> tokenise_positive_int)
tokenise_positive_int :: Tokeniser Token
tokenise_positive_int =
Positive_int_token <$> read <$> ((:) <$> parse_token' nonzero_nat_char <*> parse_many (parse_token' nat_char))
tokenise_whitespace :: Tokeniser ()
tokenise_whitespace = parse_token Whitespace_char
tokenise_zero :: Tokeniser Token
tokenise_zero =
do
parse_token Zero_char
return Zero_token
transform_notes_in :: Notes_in' -> Notes_in
transform_notes_in maybe_notes =
case maybe_notes of
Notes_in' notes -> Notes_in notes Set.empty
Tie_in' notes -> Notes_in Set.empty notes
union_notes_in :: Notes_in -> Notes_in -> Notes_in
union_notes_in (Notes_in notes_0 ties_0) (Notes_in notes_1 ties_1) =
Notes_in (Set.union notes_0 notes_1) (Set.union ties_0 ties_1)
wider_hand_span :: Keyboard (Set (Note Pitched)) -> Int
wider_hand_span keyboard = maximum (hand_span <$> keyboard)
wider_hand_note_count :: Keyboard (Set (Note Pitched)) -> Int
wider_hand_note_count keyboard = Set.size (maximumBy (comparing hand_span) keyboard)