\section{Outputting MIDI Files} \label{output} The functions in this module allow {\tt MidiFile}s to be made into Standard MIDI files (*.mid) that can be read and played by music programs such as Cakewalk. \begin{verbatim} > module Haskore.OutputMidi (outputMidiFile, midiFileToString) where > import Haskore.MidiFile > import Haskore.IOExtensions (writeBinaryFile) > import Haskore.Monads (Output, runO, outO) > import Haskore.Bitops (bSplitAt, someBytes) > import Data.Ix \end{verbatim} {\tt OutputMidiFile} is the main function for writing {\tt MidiFile} values to an actual file; its first argument is the filename: \begin{verbatim} > outputMidiFile :: String -> MidiFile -> IO () > outputMidiFile fn mf = writeBinaryFile fn (midiFileToString mf) \end{verbatim} \begin{exercise} Take as many examples as you like from the previous sections, create one or more {\tt UserPatchMaps}, write the examples to a file, and play them using a conventional Midi player. \end{exercise} Appendix \ref{test-functions} defines some functions which should make the above exercise easier. Appendices \ref{examples}, \ref{chick}, and \ref{self-similar} contain more extensive examples. Midi files are first converted to a monadic string computation using the function {\tt outMF}, and then "executed" using {\tt runM :: MidiWriter a -> String}. \begin{verbatim} > midiFileToString :: MidiFile -> String > midiFileToString = runM . outMF > > outMF :: MidiFile -> MidiWriter () > outMF (MidiFile mft divisn trks) = > do > outChunk "MThd" (do > out 2 mft -- format (type 0, 1 or 2) > out 2 (length trks) -- length of tracks to come > outputDivision divisn) -- time unit > outputTracks trks > > outputDivision :: Division -> MidiWriter () > outputDivision (Ticks nticks) = out 2 nticks > outputDivision (SMPTE mode nticks) = do > out 1 (256-mode) > out 1 nticks > > outputTracks :: [Track] -> MidiWriter () > outputTracks trks = mapM_ outputTrack trks > > outputTrack :: Track -> MidiWriter () > outputTrack trk = outChunk "MTrk" (mapM_ outputEvent (delta trk)) \end{verbatim} {\tt delta} converts a track using absolute time to one using delta time, adding EndOfTrack if not already there. \begin{verbatim} > delta :: Track -> Track > delta [] = [] > delta trk | notEOT (last trk) = trk' ++ [MetaEvent 0 EndOfTrack] > | otherwise = trk' > where > (t,trk') = mscanl delta' 0 trk > delta' :: Int -> -- current time > MEvent -> -- event > (Int, -- new time > MEvent) -- event > delta' t (MidiEvent dt e) = (dt , MidiEvent (dt-t) e) > delta' t (MetaEvent dt e) = (dt , MetaEvent (dt-t) e) > notEOT (MetaEvent _ EndOfTrack) = False > notEOT _ = True \end{verbatim} The following functions encode various {\tt MidiFile} elements into the raw data of a standard MIDI file. \begin{verbatim} > outputEvent :: MEvent -> MidiWriter () > outputEvent (MidiEvent dt mevent) = do > outVar dt > outputMidiEvent mevent > outputEvent (MetaEvent dt mevent) = do > outVar dt > outputMetaEvent mevent > outputEvent _ = outStr "" > > outputMidiEvent :: MidiEvent -> MidiWriter () > outputMidiEvent (NoteOff c p v) = outChan 128 c [p,v] > outputMidiEvent (NoteOn c p v) = outChan 144 c [p,v] > outputMidiEvent (PolyAfter c p pr) = outChan 160 c [p,pr] > outputMidiEvent (Control c cn cv) = outChan 176 c [cn,cv] > outputMidiEvent (ProgChange c pn) = outChan 192 c [pn] > outputMidiEvent (MonoAfter c pr) = outChan 208 c [pr] > outputMidiEvent (PitchBend c pb) = outChan 224 c [lo,hi] -- small-endian!! > where (hi,lo) = bSplitAt 8 pb > > -- output a channel event > outChan :: Int -> MidiChannel -> [Int] -> MidiWriter () > outChan code chan bytes = do > out 1 (code+chan) > mapM_ (out 1) bytes > > > outMeta :: Int -> [Int] -> MidiWriter () > outMeta code bytes = do > out 1 255 > out 1 code > outVar (length bytes) > outList bytes > > outMetaStr :: Int -> String -> MidiWriter () > outMetaStr code bytes = do > out 1 255 > out 1 code > outVar (length bytes) > outStr bytes > > -- As with outChunk, there are other ways to do this - but > -- it's not obvious which is best or if performance is a big issue. > outMetaMW :: Int -> MidiWriter a -> MidiWriter a > outMetaMW code m = do > out 1 255 > out 1 code > outVar (mLength m) > m > > outputMetaEvent :: MetaEvent -> MidiWriter () > outputMetaEvent (SequenceNum num) = outMetaMW 0 (out 2 num) > outputMetaEvent (TextEvent s) = outMetaStr 1 s > outputMetaEvent (Copyright s) = outMetaStr 2 s > outputMetaEvent (TrackName s) = outMetaStr 3 s > outputMetaEvent (InstrName s) = outMetaStr 4 s > outputMetaEvent (Lyric s) = outMetaStr 5 s > outputMetaEvent (Marker s) = outMetaStr 6 s > outputMetaEvent (CuePoint s) = outMetaStr 7 s > outputMetaEvent (MIDIPrefix c) = outMeta 32 [c] > outputMetaEvent EndOfTrack = outMeta 47 [] > > outputMetaEvent (SetTempo tp) = outMetaMW 81 (out 3 tp) > outputMetaEvent (SMPTEOffset hr mn se fr ff) > = outMeta 84 [hr,mn,se,fr,ff] > outputMetaEvent (TimeSig n d c b) = outMeta 88 [n,d,c,b] > outputMetaEvent (KeySig sf mi) = outMeta 89 [convert sf, fromMode mi] > where k = index (KeyCf,KeyCs) sf - 7 > convert sf = if (k >= 0) then k > else 255+k > outputMetaEvent (SequencerSpecific codes) > = outMeta 127 codes > outputMetaEvent (Unknown s) = outMetaStr 21 s \end{verbatim} The midiwriter accumulates a String. For all the usual reasons, the String is represented by ShowS. \begin{verbatim} > type MidiWriter a = Output Char a > > out :: Int -> Int -> MidiWriter () > outVar :: Int -> MidiWriter () > outList :: [Int] -> MidiWriter () > outStr :: String -> MidiWriter () > > runM :: MidiWriter a -> String > runM m = snd (runO m) > > mLength :: MidiWriter a -> Int > mLength m = length (runM m) > > out 1 x = outO [toEnum x] > out a x = mapM_ (out 1) (someBytes a x) > > outStr cs = outO cs > > outList xs = outStr (map toEnum xs) \end{verbatim} Numbers of variable size are represented by sequences of 7-bit blocks tagged (in the top bit) with a bit indicating: (1) that more data follows; or (0) that this is the last block. \begin{verbatim} > outVar n = do > outVarAux leftover > out 1 data7 > where (leftover, data7) = bSplitAt 7 n > outVarAux 0 = return () > outVarAux x = do > outVarAux leftover' > out 1 (128+data7') --make signal bit 1 > where (leftover',data7') = bSplitAt 7 x > > fromMode :: Mode -> Int > fromMode Major = 0 > fromMode Minor = 1 > > -- Note: here I've chosen to compute the track twice > -- rather than store it. Other options are worth exploring. > > outChunk :: String -> MidiWriter a -> MidiWriter a > outChunk tag m | length tag == 4 = do > outStr tag > out 4 (mLength m) > m \end{verbatim} Mapping scan (used in function delta): \begin{verbatim} x xs | | V V +---+ +----------+ l -> | f | -> m -> | mscanl f | -> r +---+ +----------+ | | V V y ys > mscanl :: (a -> b -> (a,c)) -> a -> [b] -> (a,[c]) > mscanl f l [] = (l,[]) > mscanl f l (x:xs) = let (m, y ) = f l x > (r, ys) = mscanl f m xs > in (r, y:ys) \end{verbatim}