module Scribo.MEI where
import Data.List
(transpose)
import Aedifico
import Lectio
import Cogito
import Cogito.Musarithmetic
enbrace :: String
-> String
-> String
-> String
enbrace start end contents = start ++ contents ++ end
xmltagOpen :: String
-> String
xmltagOpen name = enbrace "<" ">" name
xmltagClose :: String
-> String
xmltagClose name = enbrace "</" "> " name
xmlWrap :: String
-> [String]
-> String
-> String
xmlWrap open contents close = xmltagOpen open
++ unwords contents
++ xmltagClose close
xmlWrapBasic :: String
-> [String]
-> String
xmlWrapBasic name contents = xmlWrap name contents name
element :: String
-> [String]
-> String
element name values = xmlWrapBasic name values
attr :: String
-> String
-> String
attr name value = name ++ enbrace "=\"" "\"" value
elementAttr :: String
-> [String]
-> [String]
-> String
elementAttr name attrs values = xmlWrap (unwords [name, unwords attrs]) values name
note2mei :: Note -> String
note2mei note | isPitchRest pitch = meiRest
| otherwise = meiNote
where
pitch = notePitch note
syllable = noteSyllable note
meiRest = elementAttr "rest" [meiDur pitch] []
meiNote = elementAttr "note"
[ fn pitch | fn <- [meiPname, meiOct, meiDur] ]
[ meiAccid pitch
, element "verse" [meiSyllable syllable]
]
meiPname :: Pitch -> String
meiPname p = attr "pname" c
where c = case pnum p of
PCc -> "c"
PCd -> "d"
PCe -> "e"
PCf -> "f"
PCg -> "g"
PCa -> "a"
PCb -> "b"
PCc8 -> "c"
_ -> error $ unwords ["Unknown pitch", show $ pnum p]
meiOct :: Pitch -> String
meiOct p = attr "oct" $ show $ oct p
meiDur :: Pitch -> String
meiDur p = unwords [durAttr, dotsAttr]
where
durAttr = attr "dur" $ durString $ dur p
durString :: Dur -> String
durString d | d == DurNil = "_"
| d `elem` [Lg, LgD, LgR] = "long"
| d `elem` [Br, BrD, BrR] = "breve"
| d `elem` [Sb, SbD, SbR] = "1"
| d `elem` [Mn, MnD, MnR] = "2"
| d `elem` [Sm, SmD, SmR] = "4"
| d `elem` [Fs, FsD, FsR] = "8"
| otherwise = error $ unwords ["Unknown duration", show d]
dotsAttr | dur p `elem` [LgD, BrD, SbD, MnD, SmD, FsD] = attr "dots" "1"
| otherwise = ""
meiAccid :: Pitch -> String
meiAccid p = case accidType p of
None -> ""
Written -> elementAttr "accid"
[ attr "accid" accidString ]
[]
Implicit -> elementAttr "accid"
[ attr "accid.ges" accidString ]
[]
Suggested -> elementAttr "accid"
[ attr "accid" accidString
, attr "func" "edit"
]
[]
where accidString = case accid p of
Fl -> "f"
Na -> "n"
Sh -> "s"
_ -> error $ unwords ["unknown accid", show $ accid p]
meiSyllable :: Syllable -> String
meiSyllable syl = case sylPosition syl of
Tacet -> ""
Only -> element "syl" [text]
_ -> elementAttr "syl"
[unwords [sylConnector "d", sylPos2mei $ sylPosition syl] ]
[text]
where
text = sylText syl
sylConnector :: String -> String
sylConnector value = attr "con" value
sylPos2mei :: SyllablePosition -> String
sylPos2mei pos = attr "wordpos" meiPos
where meiPos = case pos of
First -> "i"
Middle -> "m"
Last -> "t"
_ -> error $ unwords ["Unknown syllable position", show pos]
data ListPosition = ListHead
| ListBody
| ListEnd
| ListOnly
deriving (Enum, Show, Eq)
positionMap :: ((ListPosition, a1) -> [a2]) -> [a1] -> [a2]
positionMap fn ls = concat $ map fn $ markedEnds ls
markedEnds :: [a] -> [(ListPosition, a)]
markedEnds [] = []
markedEnds (a:[]) = [ (ListOnly, a) ]
markedEnds (a:b:[]) = [ (ListHead, a)
, (ListEnd, b)]
markedEnds (a:b:cs) = [ (ListHead, a) ]
++ (map (\x -> (ListBody, x)) $ init (b:cs))
++ [(ListEnd, last cs)]
phrase2mei :: (ListPosition, MusicPhrase) -> String
phrase2mei (position, phrase) | position `elem` [ListEnd, ListOnly]
= meiNotes
| otherwise
= meiNotes ++ meiBarline ""
where meiNotes = concat $ map note2mei $ notes phrase
sentence2mei :: (ListPosition, MusicSentence) -> String
sentence2mei (position, sent) | position `elem` [ListEnd, ListOnly]
= meiPhrases
| otherwise
= meiPhrases ++ meiBarline ""
where meiPhrases = unwords $ map phrase2mei $ markedEnds sent
section2mei :: Arca -> (ListPosition, MusicSection) -> String
section2mei arca (position, sec) =
unwords [ scoreDef
, elementAttr "staff"
[ attr "n" $ show voiceNum
, attr "corresp" $ show voiceName
]
[ tempo
, elementAttr "layer"
[ attr "n" "1" ]
[ meiSentencesWithBar ]
]
]
where
voiceNum = 1 + fromEnum voiceName
voiceName = secVoiceID sec
scoreDef | position `elem` [ListHead, ListOnly]
= ""
| otherwise
= element "scoreDef"
[ mensur ++ key ]
tempo = meiMidiTempo meter
mensur = meiMeter meter
meter = arkMusicMeter config
key = meiKey (arkTone config) $ systems arca
config = secConfig sec
meiSentencesWithBar | position `elem` [ListEnd, ListOnly]
= meiSentences ++ meiFinalBar
| otherwise
= meiSentences ++ meiDoubleBar
meiSentences = unwords $ map sentence2mei $ markedEnds sentences
sentences = secSentences sec
chorus2mei :: Arca -> (ListPosition, MusicChorus) -> String
chorus2mei arca (position, chorus) = element "section" [ music ]
where
config = secConfig $ cantus chorus
music = unwords $ map (\c -> section2mei arca (position, c)) choruses
choruses = chorus2list chorus
meiKey :: Tone -> ToneSystem -> String
meiKey tone toneSystem = elementAttr "keySig"
[ attr "sig" $ meiKeySigString tone toneSystem ]
[]
meiKeyAttr :: Tone -> ToneSystem -> String
meiKeyAttr tone toneSystem = attr "key.sig" $ meiKeySigString tone toneSystem
meiKeySigString :: Tone -> ToneSystem -> String
meiKeySigString tone toneSystem | toneMollis tone toneSystem = "1f"
| otherwise = "0"
meiMeter :: MusicMeter -> String
meiMeter = meiMeterMensural
meiMeterAttr :: MusicMeter -> String
meiMeterAttr = meiMeterMensuralAttr
meiMeterModern :: MusicMeter -> String
meiMeterModern meter = elementAttr "meterSig"
[ attr "count" count
, attr "unit" unit
]
[]
where
count = show $ fst meterValues
unit = show $ snd meterValues
meterValues = case meter of
Duple -> (4, 2)
TripleMajor -> (3, 1)
TripleMinor -> (3, 2)
meiMeterMensural :: MusicMeter -> String
meiMeterMensural meter = elementAttr "mensur" [ mensur ] []
where
mensur = unwords $ case meter of
Duple -> meterCutC
TripleMinor -> meterC3
TripleMajor -> meterCutC3
meterCutC = [ imperfectTempus
, allaBreve
]
meterC3 = [ imperfectTempus
, minorProportion
]
meterCutC3 = [ imperfectTempus
, allaBreve
, minorProportion
]
imperfectTempus = unwords [ attr "sign" "C"
, attr "tempus" "2"
]
minorProportion = attr "num" "3"
allaBreve = attr "slash" "1"
meiMeterMensuralAttr :: MusicMeter -> String
meiMeterMensuralAttr meter = unwords $ case meter of
Duple -> [ attr "mensur.sign" "C"
, attr "mensur.tempus" "2"
, attr "mensur.slash" "1"
]
TripleMajor -> [ attr "mensur.sign" "C"
, attr "mensur.slash" "1"
, attr "mensur.tempus" "2"
, attr "proport.num" "3"
]
TripleMinor -> [ attr "mensur.sign" "C"
, attr "mensur.tempus" "2"
, attr "proport.num" "3"
]
meiMidiTempo :: MusicMeter -> String
meiMidiTempo meter = elementAttr "tempo"
[ meiMidiBPM meter ]
[]
meiMidiBPM meter = attr "midi.bpm" $ show bpm
where bpm = case meter of
Duple -> 120
TripleMinor -> 180
TripleMajor -> 320
chorus2list :: MusicChorus -> [MusicSection]
chorus2list chorus = [fn chorus | fn <- [cantus, alto, tenor, bass]]
score2mei :: Arca -> ArkMetadata -> MusicScore -> String
score2mei arca metadata score = meiDocument title poet key meter bpm meiScore
where
title = arkTitle metadata
poet = arkWordsAuthor metadata
config = secConfig $ cantus $ head score
key = meiKeyAttr (arkTone config) $ systems arca
meter = meiMeterAttr $ arkMusicMeter config
bpm = meiMidiBPM $ arkMusicMeter config
meiScore = unwords $ map (chorus2mei arca) $ markedEnds score
meiDoubleBar :: String
meiDoubleBar = meiBarline "dbl"
meiFinalBar :: String
meiFinalBar = meiBarline "end"
meiBarline :: String
-> String
meiBarline form | form == "" = element "barLine" []
| otherwise = elementAttr "barLine"
[ attr "form" form ]
[]
_xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> "
_meiVersion = "4.0.1"
_whoami = "Arca musarithmica Athanasii Kircherii MDCL"
_Kircher = "Athanasius Kircher Societatis Iesu"
_projectDesc = "This music was generated automatically using Athanasius \
\ Kircher's Arca musarithmica, a device and system he described in 1650 for \
\ generating music by choosing from sets of predefined permutations of pitches \
\ and rhythms. This digital implementation of the ark in the Haskell \
\ programming language was created in 2021. It takes parsed texts in XML format \
\ and outputs their musical setting in MEI XML encoding."
midiInstrumentNum :: Int
-> String
midiInstrumentNum n = elementAttr "instrDef"
[ attr "midi.instrnum" $ show n ]
[]
_midiInstrument = midiInstrumentNum reedOrgan
where
organ = 19
reedOrgan = 20
trumpet = 56
trombone = 57
oboe = 68
panflute = 75
meiDocument :: String
-> String
-> String
-> String
-> String
-> String
-> String
meiDocument title poet key meter bpm sections = _xmlHeader ++
elementAttr "mei"
[ attr "xmlns" "https://www.music-encoding.org/ns/mei"
, attr "meiversion" _meiVersion
]
[ element "meiHead"
[ element "fileDesc"
[ element "titleStmt"
[ element "title"
[ title ]
, element "composer"
[ elementAttr "persName"
[ attr "role" "creator" ]
[ _whoami ]
]
, element "lyricist"
[ elementAttr "persName"
[ attr "role" "lyricist" ]
[ poet ]
]
, element "respStmt"
[ elementAttr "persName"
[ attr "role" "inventor" ]
[ _Kircher ]
]
]
, element "pubStmt"
[ element "date"
[ "Described 1650, implemented 2021" ]
, element "availability"
[ "Musical output of the ark is in the public domain." ]
]
, element "sourceDesc"
[ element "source"
[ element "bibl"
[ element "title"
[ "Musurgia universalis" ]
, element "author"
[ "Athanasius Kircher" ]
, element "imprint"
[ element "pubPlace"
[ "Rome" ]
, element "date"
[ "1650" ]
]
]
]
]
]
, element "encodingDesc"
[ element "appInfo"
[ element "application"
[ element "name"
[ "arca" ]
]
]
, element "projectDesc"
[ element "p"
[ _projectDesc ]
]
]
]
, element "music"
[ element "body"
[ element "mdiv"
[ element "score"
[ elementAttr "scoreDef"
[ bpm ]
[ element "pgHead"
[ elementAttr "rend"
[ attr "valign" "top"
, attr "halign" "center"
, attr "fontsize" "150%"
]
[ title ]
, elementAttr "rend"
[ attr "valign" "bottom"
, attr "halign" "right"
]
[ _whoami ]
, elementAttr "rend"
[ attr "valign" "bottom"
, attr "halign" "left"
]
[ poet ]
]
, elementAttr "staffGrp"
[ attr "n" "1"
, attr "bar.thru" "false"
, attr "symbol" "bracket"
]
[ elementAttr "staffDef"
[ attr "n" "1"
, attr "xml:id" "Cantus"
, attr "lines" "5"
, attr "clef.line" "2"
, attr "clef.shape" "G"
, key
, meter
]
[ _midiInstrument ]
, elementAttr "staffDef"
[ attr "n" "2"
, attr "xml:id" "Alto"
, attr "lines" "5"
, attr "clef.line" "2"
, attr "clef.shape" "G"
, attr "clef.dis" "8"
, attr "clef.dis.place" "below"
, key
, meter
]
[ _midiInstrument ]
, elementAttr "staffDef"
[ attr "n" "3"
, attr "xml:id" "Tenor"
, attr "lines" "5"
, attr "clef.line" "2"
, attr "clef.shape" "G"
, attr "clef.dis" "8"
, attr "clef.dis.place" "below"
, key
, meter
]
[ _midiInstrument ]
, elementAttr "staffDef"
[ attr "n" "4"
, attr "xml:id" "Bass"
, attr "lines" "5"
, attr "clef.line" "4"
, attr "clef.shape" "F"
, key
, meter
]
[ _midiInstrument ]
]
]
, sections
]
]
]
]
]