hmt-0.20: Music/Theory/Time/KeyKit/Parser.hs
-- | KeyKit phrase literal (constant) parser and printer.
module Music.Theory.Time.KeyKit.Parser where
import Data.Maybe {- base -}
import Text.Printf {- base -}
import qualified Text.Parsec as P {- parsec -}
import qualified Text.Parsec.String as String {- parsec -}
-- * Parser setup
-- | A 'Char' parser with no user state.
type P a = String.GenParser Char () a
-- | Run parser and return either an error string or an answer.
kk_parse_either :: P t -> String -> Either String t
kk_parse_either p = either (\m -> Left ("kk_parse: " ++ show m)) Right . P.parse p ""
-- | Run parser and report any error. Does not delete leading spaces.
kk_parse :: P t -> String -> t
kk_parse p = either (\e -> error e) id . kk_parse_either p
-- | Run p then q, returning result of p.
(>>~) :: Monad m => m t -> m u -> m t
p >>~ q = p >>= \x -> q >> return x
kk_lexeme :: P t -> P t
kk_lexeme p = p >>~ P.many P.space
kk_uint :: P Int
kk_uint = do
digits <- P.many1 P.digit
return (read digits)
kk_int :: P Int
kk_int = do
sign <- P.optionMaybe (P.char '-')
unsigned <- kk_uint
return (maybe unsigned (const (negate unsigned)) sign)
-- * Note elements parsers
kk_note_name_p :: P Char
kk_note_name_p = P.oneOf "abcdefg"
kk_midi_note_p :: P Int
kk_midi_note_p = P.char 'p' >> kk_uint
kk_rest_p :: P Char
kk_rest_p = P.char 'r'
kk_accidental_p :: P Char
kk_accidental_p = P.oneOf "+-"
kk_char_to_note_number :: Char -> Int
kk_char_to_note_number c = fromMaybe (error "kk_char_to_note_number?") (lookup c (zip "cdefgab" [0, 2, 4, 5, 7, 9, 11]))
kk_char_to_alteration :: Char -> Int
kk_char_to_alteration c = fromMaybe (error "kk_char_to_alteration?") (lookup c (zip "+-" [1, -1]))
-- > map kk_note_number_to_name [0 .. 11]
kk_note_number_to_name :: Int -> String
kk_note_number_to_name k = fromMaybe (error "kk_note_number_to_name?") (lookup k (zip [0..] (words "c c+ d e- e f f+ g a- a b- b")))
kk_named_note_number_p :: P Int
kk_named_note_number_p = do
nm <- kk_note_name_p
ac <- P.optionMaybe kk_accidental_p
return (kk_char_to_note_number nm + maybe 0 kk_char_to_alteration ac)
kk_note_number_p :: P Int
kk_note_number_p = kk_named_note_number_p P.<|> kk_midi_note_p
-- | The octave key can be elided, ordinarily directly after the note name, ie. c2.
kk_modifier_p :: P (Char, Int)
kk_modifier_p = do
c <- P.optionMaybe (P.oneOf "ovdct")
n <- kk_int
return (fromMaybe 'o' c, n)
kk_modifiers_p :: P [(Char, Int)]
kk_modifiers_p = P.many kk_modifier_p
-- * Contextual note
{- | A note where all fields are optional.
If the note number is absent it indicates a rest.
All other fields infer values from the phrase context.
-}
data Kk_Contextual_Note =
Kk_Contextual_Note
{kk_contextual_note_number :: Maybe Int
,kk_contextual_note_octave :: Maybe Int
,kk_contextual_note_volume :: Maybe Int
,kk_contextual_note_duration :: Maybe Int
,kk_contextual_note_channel :: Maybe Int
,kk_contextual_note_time :: Maybe Int}
deriving (Eq, Ord, Show)
kk_empty_contextual_note :: Kk_Contextual_Note
kk_empty_contextual_note = Kk_Contextual_Note Nothing Nothing Nothing Nothing Nothing Nothing
kk_empty_contextual_rest :: Int -> Kk_Contextual_Note
kk_empty_contextual_rest n = kk_empty_contextual_note {kk_contextual_note_duration = Just n}
{- | If t is set and is at the end time of the previous note print a preceding comma, else print t annotation.
> c = kk_empty_contextual_note {kk_contextual_note_number = Just 0, kk_contextual_time = Just 96}
> map (\t -> kk_contextual_note_pp (t, c)) [0, 96] == ["ct96",", c"]
-}
kk_contextual_note_pp :: (Int, Kk_Contextual_Note) -> String
kk_contextual_note_pp (t', Kk_Contextual_Note n o v d c t) =
let f i j = maybe "" ((if i == 'o' then id else (i :)) . show) j
(pre, t'') = if t == Just t' then (", ","") else ("", f 't' t)
in case n of
Nothing -> concat [pre, "r", f 'd' d, t'']
Just k -> concat [pre, kk_note_number_to_name k, f 'o' o, f 'v' v, f 'd' d, f 'c' c, t'']
{- | If the note number is given as p60, then derive octave of and set it, ignoring any modifier.
Note that in KeyKit c3 is p60 or middle c.
-}
kk_contextual_note_p :: P Kk_Contextual_Note
kk_contextual_note_p = do
n <- fmap Just kk_note_number_p P.<|> (kk_rest_p >> return Nothing)
m <- kk_modifiers_p
_ <- P.many P.space
let get c = lookup c m
(n', o) =
case n of
Just n'' ->
if n'' > 11
then
let (o', n''') = n'' `divMod` 12
in (Just n''', Just (o' - 2))
else (n, get 'o')
Nothing -> (Nothing, Nothing)
return (Kk_Contextual_Note n' o (get 'v') (get 'd') (get 'c') (get 't'))
kk_contextual_note_is_rest :: Kk_Contextual_Note -> Bool
kk_contextual_note_is_rest = isNothing . kk_contextual_note_number
kk_comma_p :: P Char
kk_comma_p = kk_lexeme (P.char ',')
-- | A contextual note and an is_parallel? indicator.
kk_contextual_phrase_element_p :: P (Kk_Contextual_Note, Bool)
kk_contextual_phrase_element_p = do
n <- kk_contextual_note_p
c <- P.optionMaybe kk_comma_p
return (n, isNothing c)
kk_contextual_phrase_p :: P [(Kk_Contextual_Note, Bool)]
kk_contextual_phrase_p = P.many kk_contextual_phrase_element_p
-- * Note
-- | A note with all fields required.
data Kk_Note =
Kk_Note
{kk_note_number :: Int
,kk_note_octave :: Int
,kk_note_volume :: Int
,kk_note_duration :: Int
,kk_note_channel :: Int
,kk_note_time :: Int}
deriving (Eq, Ord, Show)
kk_default_note :: Kk_Note
kk_default_note = Kk_Note 60 3 63 96 1 0
kk_note_to_initial_contextual_note :: Kk_Note -> Kk_Contextual_Note
kk_note_to_initial_contextual_note (Kk_Note n o v d c t) =
let f i j = if i == j then Nothing else Just i
in Kk_Contextual_Note (Just n) (f o 3) (f v 63) (f d 96) (f c 1) (f t 0)
kk_note_to_contextual_note :: Kk_Note -> Kk_Note -> (Int, Kk_Contextual_Note)
kk_note_to_contextual_note (Kk_Note _ o' v' d' c' t') (Kk_Note n o v d c t) =
let f i j = if i == j then Nothing else Just i
in (t' + d', Kk_Contextual_Note (Just n) (f o o') (f v v') (f d d') (f c c') (f t t'))
-- | Elide octave modifier character.
kk_note_pp :: Kk_Note -> String
kk_note_pp (Kk_Note n o v d c t) = printf "%s%dv%dd%dc%dt%d" (kk_note_number_to_name n) o v d c t
kk_decontextualise_note :: Kk_Note -> Bool -> Kk_Contextual_Note -> Either Kk_Note Int
kk_decontextualise_note (Kk_Note _ o v d c t) is_par (Kk_Contextual_Note k' o' v' d' c' t') =
let t'' = fromMaybe (if is_par then t else t + d) t'
in case k' of
Just k'' -> Left (Kk_Note k'' (fromMaybe o o') (fromMaybe v v') (fromMaybe d d') (fromMaybe c c') t'')
Nothing -> Right t''
data Kk_Phrase = Kk_Phrase { kk_phrase_notes :: [Kk_Note], kk_phrase_length :: Int } deriving (Eq, Show)
-- | This should, but does not, append a trailing rest as required.
kk_phrase_pp :: Kk_Phrase -> String
kk_phrase_pp (Kk_Phrase n _) = unwords (map kk_note_pp n)
-- | Rests are elided, their duration is accounted for in the time of the following notetaken into account.
kk_decontextualise_phrase :: [(Kk_Contextual_Note, Bool)] -> Kk_Phrase
kk_decontextualise_phrase =
let f r c p l =
case l of
[] -> Kk_Phrase (reverse r) (kk_note_time c + kk_note_duration c)
(n,p'):l' ->
case kk_decontextualise_note c p n of
Left c' -> f (c' : r) c' p' l'
Right t' -> f r (c {kk_note_time = t'}) p' l'
in f [] kk_default_note True
-- | In addition to contextual note give end time of previous note, to allow for sequence (comma) notation.
kk_recontextualise_phrase :: Kk_Phrase -> [(Int, Kk_Contextual_Note)]
kk_recontextualise_phrase p =
let f n0 n =
case n of
[] -> []
n1 : n' -> kk_note_to_contextual_note n0 n1 : f n1 n'
in case p of
Kk_Phrase [] l -> [(0, kk_empty_contextual_rest l)]
Kk_Phrase (n1 : n') _ ->
let c1 = kk_note_to_initial_contextual_note n1
in (0, c1) : f n1 n'
{- | Read KeyKit phrase constant.
> let rw = (\p -> (kk_phrase_pp p, kk_phrase_length p)) . kk_phrase_read
> rw "c" == ("c3v63d96c1t0",96)
> rw "c, r" == ("c3v63d96c1t0",192)
> rw "c, r, c3, r, p60" == ("c3v63d96c1t0 c3v63d96c1t192 c3v63d96c1t384",480)
> rw "c, e, g" == ("c3v63d96c1t0 e3v63d96c1t96 g3v63d96c1t192",288)
> rw "c2" == rw "co2"
-}
kk_phrase_read :: String -> Kk_Phrase
kk_phrase_read = kk_decontextualise_phrase . kk_parse kk_contextual_phrase_p
{- | Re-contextualise and print phrase.
> rw = kk_phrase_print . kk_phrase_read
> rw_id i = rw i == i
> rw_id "c"
> rw_id "c e g"
> rw_id "c , e , g"
> rw_id "c e g , c f a , c e g , c e- g"
> rw_id "c , e , g c4t384"
> rw "c, r, c3, r, p60" == "c ct192 ct384"
> rw "c , e , g c4t288" == "c , e , g , c4"
> rw "c r" == "c" -- ?
-}
kk_phrase_print :: Kk_Phrase -> String
kk_phrase_print = unwords . map kk_contextual_note_pp . kk_recontextualise_phrase