> {-# 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 ()