module ZMidi.Score.ToMidiScore ( midiFileToMidiScore ) where
import Data.Word ( Word8 )
import Data.Maybe ( catMaybes )
import Data.Ord ( comparing )
import Data.List ( partition, sortBy, nub )
import Control.Arrow ( first, second )
import Control.Monad.State ( State, modify, gets, evalState )
import ZMidi.Core ( MidiFile (..), MidiEvent (..)
, MidiVoiceEvent (..), MidiMetaEvent (..)
, MidiMessage, MidiTrack (..), MidiHeader (..)
, MidiTimeDivision (..)
)
import ZMidi.Score.Datatypes hiding ( TPB (..) )
import qualified ZMidi.Score.Datatypes as S ( TPB (..) )
import ZMidi.Score.Utilities
midiFileToMidiScore :: MidiFile -> MidiScore
midiFileToMidiScore mf = MidiScore (select isKeyChange keyChange NoKey meta)
(nub $ select isTimeSig tsChange NoTimeSig meta)
tb
(hdr_format . mf_header $ mf)
(select isTempoChange tempChange 500000 meta)
(gcIOId . buildTickMap $ trks)
(filter (not . null) trks) where
tb = getDivision . mf_header $ mf
(trks, meta) = second concat .
unzip . map (partition isNoteEvent . midiTrackToVoice)
. mf_tracks $ mf
select :: (Timed ScoreEvent -> Bool) -> (ScoreEvent -> a) -> a
-> [Timed ScoreEvent] -> [Timed a]
select f c def ses = case filter f ses of
[] -> [Timed 0 def]
t -> map (fmap c) t
getDivision :: MidiHeader -> S.TPB
getDivision hd = case time_division hd of
(FPS _ ) -> error "no division found"
(TPB b ) -> fromIntegral b
midiTrackToVoice :: MidiTrack -> Voice
midiTrackToVoice m =
sortBy (comparing onset) . catMaybes
$ evalState (mapM toScoreEvent . getTrackMessages $ m) (0, []) where
toScoreEvent :: MidiMessage -> State MidiState (Maybe (Timed ScoreEvent))
toScoreEvent mm@(dt, me) = do
modify (stateTimeWith (+ (fromIntegral dt)))
case me of
(VoiceEvent _ _) -> voiceEvent mm
(MetaEvent _) -> metaEvent mm
_ -> return Nothing
voiceEvent :: MidiMessage -> State MidiState (Maybe (Timed ScoreEvent))
voiceEvent mm = case getVoiceEvent mm of
Just (NoteOff chn ptch _vel) -> toMidiNote chn ptch
Just (NoteOn chn ptch 0 ) -> toMidiNote chn ptch
Just n@(NoteOn _chn _ptch _vel)
-> do t <- gets fst
modify . addMessage $ (t, n)
return Nothing
_ -> return Nothing
metaEvent :: MidiMessage -> State MidiState (Maybe (Timed ScoreEvent))
metaEvent mm =
do t <- gets fst
case getMetaEvent mm of
Just (SetTempo tp)
-> return . Just . Timed t . TempoChange . fromIntegral $ tp
Just (TimeSignature num den metr n32n)
-> return . Just . Timed t $ TimeSigChange
(TimeSig (fromIntegral num) (2 ^ den) metr n32n)
Just (KeySignature root scale)
-> return . Just $ Timed t (KeyChange (Key root scale ))
_ -> return Nothing
toMidiNote :: Word8 -> Word8 -> State MidiState (Maybe (Timed ScoreEvent))
toMidiNote c p =
do ms <- gets snd
case span (not . isNoteOnMatch c p) ms of
(_, [] ) ->
(return Nothing)
(x, (ons, noteOn) : y) ->
do modify (setMessages (x ++ y))
t <- gets fst
return . Just . Timed ons $ NoteEvent (fromIntegral c)
(toPitch p) (getVelocity noteOn) (t ons)
isNoteOnMatch :: Word8 -> Word8 -> (Time, MidiVoiceEvent) -> Bool
isNoteOnMatch offc offp (_t, NoteOn onc onp _v) = onc == offc && onp == offp
isNoteOnMatch _c _p _ = False
getVoiceEvent :: MidiMessage -> Maybe MidiVoiceEvent
getVoiceEvent (_t, (VoiceEvent _ e)) = Just e
getVoiceEvent _ = Nothing
getMetaEvent :: MidiMessage -> Maybe MidiMetaEvent
getMetaEvent (_t, (MetaEvent e)) = Just e
getMetaEvent _ = Nothing
getVelocity :: MidiVoiceEvent -> Velocity
getVelocity (NoteOn _ _ v) = Velocity v
getVelocity (NoteOff _ _ v) = Velocity v
getVelocity _ = error "not a noteOn or a noteOff event"
type MidiState = (Time, [(Time, MidiVoiceEvent)])
setMessages :: [(Time, MidiVoiceEvent)] -> MidiState -> MidiState
setMessages ms = second (const ms)
addMessage :: (Time, MidiVoiceEvent) -> MidiState -> MidiState
addMessage m = second (m :)
stateTimeWith :: (Time -> Time) -> MidiState -> MidiState
stateTimeWith f = first f