The Haskore Tutorial
top back next

6  Midi

Midi ("musical instrument digital interface") is a standard protocol adopted by most, if not all, manufacturers of electronic instruments. At its core is a protocol for communicating musical events (note on, note off, key press, etc.) as well as so-called meta events (select synthesizer patch, change volume, etc.). Beyond the logical protocol, the Midi standard also specifies electrical signal characteristics and cabling details. In addition, it specifies what is known as a standard Midi file which any Midi-compatible software package should be able to recognize.

Over the years musicians and manufacturers decided that they also wanted a standard way to refer to common or general instruments such as "acoustic grand piano," "electric piano," "violin," and "acoustic bass," as well as more exotic ones such as "chorus aahs," "voice oohs," "bird tweet," and "helicopter." A simple standard known as General Midi was developed to fill this role. It is nothing more than an agreed-upon list of instrument names along with a program patch number for each, a parameter in the Midi standard that is used to select a Midi instrument's sound.

Most "sound-blaster"-like sound cards on conventional PC's know about Midi, as well as General Midi. However, the sound generated by such modules, and the sound produced from the typically-scrawny speakers on most PC's, is often quite poor. It is best to use an outboard keyboard or tone generator, which are attached to a computer via a Midi interface and cables. It is possible to connect several Midi instruments to the same computer, with each assigned a different channel. Modern keyboards and tone generators are quite amazing little beasts. Not only is the sound quite good (when played on a good stereo system), but they are also usually multi-timbral, which means they are able to generate many different sounds simultaneously, as well as polyphonic, meaning that simultaneous instantiations of the same sound are possible.

Note: If you decide to use the General midi features of your sound-card, you need to know about another set of conventions known as "Basic Midi" which is not discussed here. The most important aspect of Basic Midi is that Channel 10 is dedicated to percussion. A future release of Haskore should make these distinctions more concrete.

Haskore provides a way to specify a Midi channel number and General Midi instrument selection for each IName in a Haskore composition. It also provides a means to generate a Standard Midi File, which can then be played using any conventional Midi software. In this section the top-level code needed by the user to invoke this functionality will be described, along with the gory details.

> module HaskToMidi (module HaskToMidi, module GeneralMidi, module MidiFile)
>        where
>
> import Basics
> import Performance
> import MidiFile
> import GeneralMidi
> import List(partition)
> import Char(toLower,toUpper)

Instead of converting a Haskore Performance directly into a Midi file, Haskore first converts it into a datatype that represents a Midi file, which is then written to a file in a separate pass. This separation of concerns makes the structure of the Midi file clearer, makes debugging easier, and provides a natural path for extending Haskore's functionality with direct Midi capability (in fact there is a version of Haskore that does this under Windows '95, but it is not described here).

A UserPatchMap is a user-supplied table for mapping instrument names (IName's) to Midi channels and General Midi patch names. The patch names are by default General Midi names, although the user can also provide a PatchMap for mapping Patch Names to unconventional Midi Program Change numbers.  

> type UserPatchMap = [(IName,GenMidiName,MidiChannel)]

See Appendix A for an example of a useful user patch map.

Given a UserPatchMap, a performance is converted to a datatype representing a Standard Midi File using the performToMidi function.  

> performToMidi :: Performance -> UserPatchMap -> MidiFile
> performToMidi pf pMap = 
>        MidiFile mfType (Ticks division)
>          (map (performToMEvs pMap) (splitByInst pf))

A table of General Midi assignments called genMidiMap is imported from GeneralMidi in Appendix E. The Midi file datatype itself and functions for writing it to files are imported from the module MidiFile, which is described later in this section.

Now for the Gory Details.

Some preliminaries, otherwise known as constants:  

> mfType   = 1  :: MFType    -- midi-file type 1 always used
> velocity = 80 :: Velocity  -- default velocity (max 100)
> division = 96 :: Int       -- time-code division: 96 ticks per quarter note

Since we are implementing Type 1 Midi Files, we can associate each instrument with a separate track. So first we partition the event list into separate lists for each instrument.  

> splitByInst :: Performance ->  [(IName,Performance)]
> splitByInst [] = []
> splitByInst pf = (i,pf1) : splitByInst pf2
>                  where (pf1,pf2) = partition (\e -> getEventInst e == i) pf
>                        i         = getEventInst (head pf)

The crux of the conversion process is performToMEvs, which converts a Performance into a stream of MEvents.  

> performToMEvs :: UserPatchMap -> (IName,Performance) -> [MEvent]
> performToMEvs pMap (inm,perf) =
>   let (midiChan,progNum) = unMap pMap inm
>       setupInst          = MidiEvent 0 (ProgChange midiChan progNum)
>       loop []     = []
>       loop (e:es) = let (mev1,mev2) = mkMEvents midiChan e
>                     in  mev1 : insertMEvent mev2 (loop es)
>   in  setupInst : loop perf

A source of incompatibilty between Haskore and Midi is that Haskore represents notes with an onset and a duration, while Midi represents them as two separate events, an note-on event and a note-off event. Thus MkMEvents turns a Haskore Event into two MEvents, a NoteOn and a NoteOff.  

> mkMEvents :: MidiChannel -> Event -> (MEvent,MEvent)  
> mkMEvents mChan (Event t i p d v) =
>                     ( MidiEvent (toDelta t)     (NoteOn  mChan p v'),
>                       MidiEvent (toDelta (t+d)) (NoteOff mChan p v') )
>           where v' = min 127 (round (v*1.27))
>
> toDelta t = round (t * 4.0 * float division)

The final critical function is insertMEvent, which inserts an MEvent into an already time-ordered sequence of MEvents.  

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

The following functions lookup IName's in UserPatchMaps to recover channel and program change numbers. Note that the function that does string matching ignores case, and allows substring matches. For example, "chur" matches "Church Organ". Note also that the first match succeeds, so using a substring should be done with care to be sure that the correct instrument is selected.  

> unMap :: UserPatchMap -> IName -> (MidiChannel,ProgNum)
> unMap pMap iName = (channel, gmProgNum gmName)
>   where (gmName, channel) = lookup iName pMap
>         lookup x ((y,z,q):ys) = if (x `partialMatch` y) then (z,q) else lookup x ys
>         lookup x []           = error ("Instrument " ++ x ++ " unknown")
>
> gmProgNum :: GenMidiName -> ProgNum
> gmProgNum gmName = lookup gmName genMidiMap
>   where lookup x ((y,z):ys) = if (x `partialMatch` y) then z else lookup x ys
>         lookup x []         = error ("Instrument " ++ x ++ " unknown")
>
> partialMatch       :: String -> String -> Bool
> partialMatch s1 s2 = 
>   let s1' = map toLower s1;  s2' = map toLower s2
>       len = min (length s1) (length s2)
>   in take len s1' == take len s2'


The Haskore Tutorial
top back next