> {-# LINE 8 "ToMidi.lhs" #-}

> module Euterpea.IO.MIDI.ToMidi where

> import Euterpea.Music
> import Euterpea.IO.MIDI.MEvent
> import Euterpea.IO.MIDI.GeneralMidi
> import Euterpea.IO.MIDI.MidiIO
> import Euterpea.IO.MIDI.ExportMidiFile
> import Sound.PortMidi
> import Data.List(partition)
> import Data.Char(toLower,toUpper)
> import Codec.Midi

> type ProgNum     = Int

> type UserPatchMap = [(InstrumentName, Channel)]

> makeGMMap :: [InstrumentName] -> UserPatchMap
> makeGMMap ins = mkGMMap 0 ins
>   where mkGMMap _ []        = []
>         mkGMMap n _ | n>=15 =
>                   error "makeGMMap: too many instruments."
>         mkGMMap n (Percussion : ins)    =
>                   (Percussion, 9) : mkGMMap n ins
>         mkGMMap n (i : ins) =
>                   (i, chanList !! n) : mkGMMap (n+1) ins
>         chanList = [0..8] ++ [10..15]  --  channel 9 is for percussion


> upmLookup :: UserPatchMap  -> InstrumentName
>                            -> (Channel, ProgNum)
> upmLookup upm iName = (chan, toGM iName)
>   where chan = maybe  (error (  "instrument " ++ show iName ++
>                                 " not in patch map")  )
>                       id (lookup iName upm)

> toMidi :: [MEvent] -> Midi
> toMidi = toMidiUPM defUpm

> toMidiUPM :: UserPatchMap -> [MEvent] -> Midi
> toMidiUPM upm pf =
>    let split     = splitByInst pf
>        insts     = map fst split
>        rightMap  =  if (allValid upm insts) then upm
>                     else (makeGMMap insts)
>    in Midi  (if length split == 1  then SingleTrack
>                                    else MultiTrack)
>             (TicksPerBeat division)
>             (map (fromAbsTime . mevsToMessages rightMap) split)

> division = 96 :: Int

> allValid :: UserPatchMap -> [InstrumentName] -> Bool
> allValid upm = and . map (lookupB upm)

> lookupB :: UserPatchMap -> InstrumentName -> Bool
> lookupB upm x = or (map ((== x) . fst) upm)

> splitByInst :: [MEvent] ->  [(InstrumentName, [MEvent])]
> splitByInst [] = []
> splitByInst pf = (i, pf1) : splitByInst pf2
>        where i          = eInst (head pf)
>              (pf1, pf2) = partition (\e -> eInst e == i) pf

> type MidiEvent = (Ticks, Message)

> defST = 500000

> mevsToMessages ::  UserPatchMap
>                   -> (InstrumentName, [MEvent])
>                   -> [MidiEvent]
> mevsToMessages upm (inm, pf) =
>   let  (chan,progNum)   = upmLookup upm inm
>        setupInst        = (0, ProgramChange chan progNum)
>        setTempo         = (0, TempoChange defST)
>        loop []      =  []
>        loop (e:es)  =  let (mev1,mev2) = mkMEvents chan e
>                        in mev1 : insertMEvent mev2 (loop es)
>   in setupInst : setTempo : loop pf


> mkMEvents :: Channel -> MEvent -> (MidiEvent,MidiEvent)
> mkMEvents  mChan (MEvent {  eTime = t, ePitch = p,
>                            eDur = d, eVol = v})
>                   = (  (toDelta t, NoteOn  mChan p v'),
>                        (toDelta (t+d), NoteOff mChan p v') )
>            where v' = max 0 (min 127 (fromIntegral v))

> toDelta t = round (t * 2.0 * fromIntegral division)

> insertMEvent :: MidiEvent -> [MidiEvent] -> [MidiEvent]
> insertMEvent mev1  []         = [mev1]
> insertMEvent mev1@(t1,_) mevs@(mev2@(t2,_):mevs') =
>       if t1 <= t2 then mev1 : mevs
>                   else mev2 : insertMEvent mev1 mevs'

> defUpm :: UserPatchMap
> defUpm = [(AcousticGrandPiano,0),
>           (Marimba,1),
>           (Vibraphone,2),
>           (AcousticBass,3),
>           (Flute,4),
>           (TenorSax,5),
>           (AcousticGuitarSteel,6),
>           (Viola,7),
>           (StringEnsemble1,8),
>           (AcousticGrandPiano,9)]
>            --  the GM name for drums is unimportant, only channel 9



> writeMidi :: ToMusic1 a => FilePath -> Music a -> IO ()
> writeMidi fn m = exportMidiFile fn $ toMidi $ perform m

 play :: ToMusic1 a => Music a -> IO ()
 play = playM . toMidi . perform

 playM :: Midi -> IO ()
 playM midi = do
   initialize
   (defaultOutput playMidi) midi
   terminate
   return ()