content: > module Euterpea.IO.MIDI.FromMidi (fromMidi) where
  > import Euterpea.Music
  > import Euterpea.IO.MIDI.ToMidi
  > import Euterpea.IO.MIDI.GeneralMidi
  > import Data.List
  > import Codec.Midi
  
  
  Donya Quick
  Last updated 15-Oct-2013.
  
  Changes since last major version (15-Jan-2013):
  - makeUPM: (is !! i, 10) changed to (is !! i, 9) for Percussion.
  - Instrument numbers <0 are interpreted as Percussion.
  - ProgChange 10 x is now assigned (-1) as an instrument number.
  
  KNOWN ISSUES:
  - Tempo changes occuring between matching note on/off events may not be 
    interpreted optimally. A performance-correct representation rather 
    than a score-correct representation could be accomplished by looking 
    for these sorts of between-on-off tempo changes when calculating a 
    note's duration. 
    
  This code was originally developed for research purposes and then 
  adapted for CPSC 431/531 to overcome some problems exhibited by the
  original implementation of fromMidi. 
  
  This code has functions to read Midi values into an intermediate type,
  SimpleMsg, before conversion to Music (Pitch, Volume) to make processing 
  instrument changes easier. The following features will be retained from 
  the input file:
  - Placement of notes relative to the beat (assumed to be quarternotes).
  - The pitch, volume, and instrument of each note.
  - Tempo changes indicated by TempoChange MIDI events
  
  Other MIDI controller information is currently not supported. This includes 
  events such as pitch bends and modulations. For these controllers, there is 
  no simple way to capture the information in a Music data structure.
  
  The following datatype is for a simplification of MIDI events into simple 
  On/off events for pitches occurring at different times. There are two 
  types of events considered: tempo changes and note events. The note events
  are represented by tuples of:
  - exact onset time, Rational
  - absolute pitch, AbsPitch
  - volume from 0-127, Volume
  - instrument number, Int. The value (-1) is used for Percussion.
  - on/off type, NEvent
  
  > data NEvent = On | Off
  >   deriving (Eq, Show, Ord)
  
  > data SimpleMsg = SE (Rational, AbsPitch, Volume, Int, NEvent) |
  >               T (Rational, Rational)
  >   deriving (Eq, Show)
  > instance Ord (SimpleMsg) where
  >     compare (SE(t,p,v,i,e)) (SE(t',p',v',i',e')) = 
  >         if t<t' then LT else if t>t' then GT else EQ
  >     compare (T(t,x)) (SE(t',p',v',i',e')) = 
  >         if t<t' then LT else if t>t' then GT else EQ
  >     compare (SE(t,p,v,i,e)) (T(t',x)) = 
  >         if t<t' then LT else if t>t' then GT else EQ
  >     compare (T(t,x)) (T(t',x')) =
  >         if t<t' then LT else if t>t' then GT else EQ
  
  The importFile function places track ticks (Ticks) in a format where 
  each value attached to a message represents the number of ticks that 
  have passed SINCE THE LAST MESSAGE. The following function will convert 
  input in that format into a list of pairs where the ticks are absolute. 
  In otherwords, ticks in the output will represent the exact point in 
  time of an event. This means that unsupported events (e.g. pitch bend) 
  can later be filtered out without affecting the timing of support events.
  
  > addTrackTicks :: Int -> [(Ticks, a)] -> [(Ticks, a)]
  > addTrackTicks sum [] = []
  > addTrackTicks sum ((t,x):ts) = (t+sum,x) : addTrackTicks (t+sum) ts
  
  The following function addresses a ticks to Music duration conversion.
  
  > applyTD :: TimeDiv -> SimpleMsg -> SimpleMsg
  > applyTD tdw x = 
  >     case x of T(t,i) -> T(fixT tdw t, i) 
  >               SE(t,p,v,i,e) -> SE(fixT tdw t, p, v, i, e) where
  
  > fixT tdw t = 
  >     case tdw of TicksPerBeat td -> t / (fromIntegral td * 4)
  >                 TicksPerSecond fps tpf -> t / fromIntegral (fps * tpf)
  
  
  The midiToEvents function will take a Midi structure (from importFile, 
  for example) and convert it to a list of lists of SimpleMsgs. Each outer 
  list represents a track in the original Midi. 
  
  > midiToEvents :: Midi -> [[SimpleMsg]]
  > midiToEvents m = 
  >     let ts = map (simplifyTrack 0) $ map (addTrackTicks 0) (tracks m) 
  >     in  distributeTempos $ map (map (applyTD $ timeDiv m)) ts where 
  >   simplifyTrack :: Int -> [(Ticks, Message)] -> [SimpleMsg]
  >   simplifyTrack icur [] = []
  >   simplifyTrack icur ((t,m):ts) = 
  >     case m of (NoteOn c p v) -> 
  >                   SE (fromIntegral t, p, v, icur, On) : simplifyTrack icur ts
  >               (NoteOff c p v) -> 
  >                   SE (fromIntegral t, p, v, icur, Off) : simplifyTrack icur ts
  >               (ProgramChange c p) -> simplifyTrack (if c==9 then (-1) else p) ts 
  >               (TempoChange x) -> T (fromIntegral t, fromIntegral x) : simplifyTrack icur ts
  >               _ -> simplifyTrack icur ts 
  
  
  The first track is the tempo track. It's events need to be distributed
  across the other tracks. This function below is called for that purpose
  in midiToEvents above.
  
  > distributeTempos :: [[SimpleMsg]] -> [[SimpleMsg]]
  > distributeTempos tracks = 
  >     if length tracks > 1 then map (sort . (head tracks ++)) (tail tracks)
  >     else tracks -- must be a single-track file with embedded tempo changes.
  
  
  The eventsToMusic function will convert a list of lists of SimpleMsgs 
  (output from midiToEvents) to a Music(Pitch,Volume) structure. All 
  notes will be connected together using the (:=:) constructor. For 
  example, the first line of "Frere Jaque", which would normally be
  written as:
  
  c 5 qn :+: d 5 qn :+: e 5 qn :+: c 5 qn
  
  would actually get represented like this when read in from a MIDI:
  
  	(rest 0 :+: c 5 qn) :=:
        (rest qn :+: d 5 qn) :=:
        (rest hn :+: e 5 qn) :=:
        (rest dhn :+: c 5 qn)
  
  This structure is clearly more complicated than it needs to be.
  However, identifying melodic lines and phrases inorder to group the
  events in a more musically appropriate manor is non-trivial, since
  it requires both phrase and voice identification within an instrument 
  To see why this is the case, consider a Piano, which may have right 
  and lef thand lines that might be best separated by :=: at the 
  outermost level. In a MIDI, however, we are likely to get all of the
  events for both hands lumped into the same track. 
  
  The parallelized structure is also required for keeping tempo changes
  syced between instruments. While MIDI files allow tempo changes to 
  occur in the middle of a note, Euterpea's Music values do not.
        
  Instruments will be grouped at the outermost level. For example, if 
  there are 2 instruments with music values m1 and m2 repsectively, the
  structure would be:
  
      (instrument i1 m1) :=: (instrument i2 m1)
  	
  Tempo changes are processed within each instrument.
  
  > eventsToMusic :: [[SimpleMsg]] -> Music (Pitch, Volume)
  > eventsToMusic tracks = 
  >     let tracks' = splitByInstruments tracks -- handle any mid-track program changes
  >         is = map toInstr $ map getInstrument $ filter (not.null) tracks' -- instruments
  >         tDef = 500000 -- current tempo, 120bpm as microseconds per qn
  >     in  chord $ zipWith instrument is $ map (seToMusic tDef) tracks' where
  >   
  >   toInstr :: Int -> InstrumentName
  >   toInstr i = if i<0 then Percussion else toEnum i 
  >
  >   seToMusic :: Rational -> [SimpleMsg] -> Music (Pitch, Volume)
  >   seToMusic tCurr [] = rest 0
  >   seToMusic tCurr (e1@(SE(t,p,v,ins,On)):es) = 
  >     let piMatch (SE(t1,p1,v1,ins1,e1)) = (p1==p && ins1==ins) && e1==Off
  >         piMatch (T(t1,x)) = False
  >         is = findIndices piMatch es -- find mactching note-offs
  >         SE(t1,p1,v1,ins1, e) = es !! (is !! 0) -- pick the first matching note-off
  >         n = (rest t :+: note (t1-t) (pitch p,v)) -- create a Music note
  >     in  if v > 0 then -- a zero volume note is silence
  >              if length is > 0 then n :=: seToMusic tCurr es -- found an off
  >              else seToMusic tCurr ((e1:es)++[correctOff e1 es]) -- missing off case
  >         else seToMusic tCurr es
  >   seToMusic tCurr (e1@(T (t,newTempo)):es) = 
  >     let t2 = getTime $ head es -- find time of next event after tempo change
  >         tfact = tCurr / newTempo -- calculate tempo change factor
  >         es' = map (changeTime (subtract t)) es -- adjust start times
  >         m = rest t :+: tempo tfact (seToMusic newTempo es')  
  >     in  if null es then rest 0 else m where
  >         changeTime f (SE (t,p,v,i,e)) = SE (f t,p,v,i,e)
  >         changeTime f (T (t,x)) = T (f t, x)
  >   seToMusic tCurr (_:es) = seToMusic tCurr es -- ignore note-offs (already handled)
  
  
  Finding the time of an event.
  
  > getTime (SE(t,p,v,i,e)) = t
  > getTime (T (t,x)) = t
  
  
  Finding the instrument associated with a track. Only the first
  instrument label to appear is chosen. If a program change happens
  mid-track, it will not be counted.
  
  > getInstrument ((SE(t,p,v,i,e)):xs) = i
  > getInstrument ((T x) : xs) = getInstrument xs
  > getInstrument [] = -1 -- No instrument assigned
  
  
  The following function ensure that only one instrument appears in 
  each list of SimpleMsgs. This is necessary in order to ensure that 
  instrument assignments occur at the outermost level of the Music.
  
  > splitByInstruments :: [[SimpleMsg]] -> [[SimpleMsg]] 
  > splitByInstruments [] = []
  > splitByInstruments (t:ts) = 
  >     let i = getInstrument t
  >         (t',t'') = splitByI i t
  >         ts' = if or $ map isSE t'' then splitByInstruments (t'':ts) 
  >               else splitByInstruments ts
  >     in  if or $ map isSE t' then t' : ts' else ts'
  
  > isSE :: SimpleMsg -> Bool
  > isSE (SE xs) = True
  > isSE (T i) = False
  
  
  The splitByI function partitions a stream to select a specific instrument's events.
  
  > splitByI :: Int -> [SimpleMsg] -> ([SimpleMsg],[SimpleMsg])
  > splitByI i0 [] = ([],[])
  > splitByI i0 (x:xs) = 
  >     let (ts,fs) = splitByI i0 xs
  >         f (SE(_,_,_,i1,_)) = i0 == i1
  >         f _ = False
  >     in  case x of SE x' -> if f x then (x:ts,fs) else (ts,x:fs)
  >                   T i -> (x:ts, x:fs) -- add tempos to both streams
  
  
  This function is an error-handling method for MIDI files which have 
  mismatched note on/off events. This seems to be common in output from 
  some software. The solution used here is to assume that the note lasts 
  until the the time of the last event in the list. 
  
  > correctOff (SE(t,p,v,ins,e)) [] = SE(t,p,v,ins,Off)
  > correctOff (SE(t,p,v,ins,e)) es = 
  >     let SE(t1,p1,v1,ins1,e1) = last $ filter isSE es
  >     in  SE(t1,p,v,ins,Off) 
  
  
  The fromMidi function wraps the combination of midiToEvents and 
  eventsToMusic and performs the final conversion to Music1.
  
  > fromMidi :: Midi -> Music1
  > fromMidi m = 
  >     let seList = midiToEvents m
  >         iNums = filter (>0) $ map getInstrument seList
  >         upm = makeUPM $ map toEnum iNums
  >     in  mMap (\(p,v) -> (p, [Volume v])) $ eventsToMusic seList
  
  
  This function is to correct for the fact that channel 10 is
  traditionally reserved for percussion. If there is no percussion,
  then channel 10 must remain empty. Channels are indexed from zero 
  in this representation, so channel 1 is 0, channel 10 is 9, etc.
  
  > makeUPM :: [InstrumentName] -> UserPatchMap
  > makeUPM is = 
  >     case findIndex (==Percussion) is of 
  >         Nothing -> zip is ([0..8]++[10..]) -- no percussion
  >         Just i -> (is !! i, 9) : 
  >                   zip (take i is ++ drop (i+1) is) ([0..8]++[10..])
  
  