Cse536 Functional Programming Lecture #19, Dec. 6, 2004 •Todays Topics –Interpreting Music –Performance –MidiFile •Read – Chapters 21 - Interpreting Functional Music – Chapter 22 – From Performance to Midi 3/16/2016 1 Cse536 Functional Programming Haskore • Haskore is a Haskell library for constructing digital music • The end result is a MIDI-file • Today’s lecture: – translating the Music datatype into a MIDI file Haskell Haskore Abstract High Level Haskore Implementation independent MIDI presentation low level bit based implementation standard 3/16/2016 The point of today’s lecture 2 Cse536 Functional Programming Musical notation cScale = line [c 4 qn, d 4 qn, e 4 qn, f 4 qn, g 4 qn, a 4 qn, b 4 qn, c 5 qn] Algorithm Note (C,4) (1 % 4) :+: (Note (D,4) (1 % 4) :+: (Note (E,4) (1 % 4) :+: (Note (F,4) (1 % 4) :+: (Note (G,4) (1 % 4) :+: (Note (A,4) (1 % 4) :+: (Note (B,4) (1 % 4) :+: (Note (C,5) (1 % 4) :+: (Rest (0 % 1))))))))) 3/16/2016 Music algebraic datatype 3 Cse536 Functional Programming Need a Shape transformation • Algebraic datatype is “tree-like” • MIDI-file is linear in shape • Need a “flattening” transformation :+: E :+: C 3/16/2016 D 4 Cse536 Functional Programming 3 step process • Translate Music data type to Performance data type – this step begins the flattening process – It uses simpler notion of time than the midi-standard – It’s purely functional (no actions) • Translate Performance data type to MidiFile data type – Introduces the MIDI notion of events. – Each note, rest etc. translates to two midi-events, » a start event » a stop event • Write MidiFile data type to a real MIDI format file – Lot’s of messy details. But luckily this is handled by the Haskell MIDI library. – First place that non-pure actions are introduced. 3/16/2016 5 Cse536 Functional Programming Performance data type type Performance = [Event] data Event = Event { eTime :: Time, eInst :: IName, ePitch :: AbsPitch, eDur :: DurT } deriving (Eq,Ord,Show) type Time type DurT 3/16/2016 -- start time -- instrument -- pitch or note -- duration = Float = Float 6 Cse536 Functional Programming Haskell record syntax data Event = Event { eTime :: Time, eInst :: IName, ePitch :: AbsPitch, eDur :: DurT } • Normal constructor notation: – (Event start-time instrument pitch duration) • Also introduces “selector functions”: – – – – eTime :: Event -> Time eInst :: Event -> Iname ePitch :: Event -> AbsPitch eDur :: Event -> DurT • And an update notation: – x {eTime = y} == Event y (eInst x) (ePitch x) (eDur x) – where x has shape (Event a b c d) 3/16/2016 7 Cse536 Functional Programming perform :: Context -> Music -> Performance • The function perform translates a Music value into a Performance in some Context – A Context contains » time to begin the performance » the proper musical “key” to play the performance » the tempo (or speed) to play the performance » the instrument to use (unless one is explicitly given) data Context = Context { cTime :: Time, cInst :: IName, cDur :: DurT, cKey :: Key } deriving Show type Key = AbsPitch • metro computes the time for one whole note, given a beats per minute setting and a duration for one beat (quarter note, half note etc). metro :: Float -> Dur -> DurT metro setting dur = 60 / (setting * ratioToFloat dur) 3/16/2016 8 Cse536 Functional Programming Simple Perform perform c@(Context t i dt k) m = case m of Note p d -> let dur = ratioToFloat d * dt in [Event t i (transpose p k i) dur] Rest d -> [] Quadratic running m1 :+: m2 -> time perform c m1 ++ perform (c {cTime = t + ratioToFloat (dur m1) * dt}) m2 m1 :=: m2 -> merge (perform c m1) (perform c m2) Tempo a m -> perform (c {cDur = dt / ratioToFloat a} ) m Trans p m -> perform (c {cKey = k + p} ) m Instr nm m -> perform (c {cInst = nm} ) m where transpose p k Percussion = absPitch p transpose p k _ = absPitch p + k 3/16/2016 9 Cse536 Functional Programming Consider a Music Tree like this: • A tree, skewed to the left, will be very expensive to translate: :+: E :+: :+: :+: B D D D E :+: m1 :+: m2 -> perform c m1 ++ perform (c {cTime = ...(dur m1)...}) m2 • Solution: compute the translation and the duration of the “Music-tree” simultaneously. • Have perform return a pair: perform :: Context -> Music -> (Performance,DurT) 3/16/2016 10 Cse536 Functional Programming Efficient perform perform :: Context -> Music -> Performance perform c m = fst (perf c m) Note how the context changes in recursive calls perf :: Context -> Music -> (Performance, DurT) perf c@(Context t i dt k) m = case m of Note p d -> let dur = ratioToFloat d * dt in ([Event t i (transpose p k i) dur], dur) Rest d -> ([], ratioToFloat d * dt) m1 :+: m2 -> let (pf1,d1) = perf c m1 (pf2,d2) = perf (c {cTime = t+d1} ) m2 in (pf1++pf2, d1+d2) m1 :=: m2 -> let (pf1,d1) = perf c m1 (pf2,d2) = perf c m2 in (merge pf1 pf2, max d1 d2) Tempo a m -> perf (c {cDur = dt / ratioToFloat a} ) m Trans p m -> perf (c {cKey = k + p} ) m Instr nm m -> perf (c {cInst = nm} ) m where transpose p k Percussion = absPitch p transpose p k _ = absPitch p + k 11 3/16/2016 Cse536 Functional Programming merge • Consider the case for parallel composition (chords etc.) m1 :=: m2 -> let (pf1,d1) = perf c m1 (pf2,d2) = perf c m2 in (merge pf1 pf2, max d1 d2) • merge - synchronizes two time stamped ordered lists merge :: Performance -> Performance -> Performance merge a@(e1:es1) b@(e2:es2) = if eTime e1 < eTime e2 then e1 : merge es1 b else e2 : merge a es2 merge [] es2 = es2 3/16/2016 merge es1 [] = es1 12 Cse536 Functional Programming Notes on step 1 • Perform has flattened the Music structure into a list of events. • Events are time stamped, and the final list is in timestamp order. • Each event carries information about instrument, pitch, and duration. • Perform has not dealt with the issue of each note etc. must be translated into two “midi-events”, one with a start, and the other with a stop. 3/16/2016 13 Cse536 Functional Programming The Haskell MIDI Library data MidiFile = MidiFile MFType Division [Track] deriving (Show, Eq) type MFType = Int type Track = [MEvent] data Division = Ticks Int | SMPTE Int Int deriving (Show,Eq) data MEvent = | | deriving MidiEvent ElapsedTime MidiEvent MetaEvent ElapsedTime MetaEvent NoEvent (Show,Eq) type ElapsedTime 3/16/2016 = Int 14 Cse536 Functional Programming Lots of details we’re ignoring MidiFile MFType Division [Track] • MFType - Int in the range {1,2,3}. We’re interested in MFType = 2. This means the midi file contains information about multiple tracks (up to 15), each playing a different instrument. All tracks are played simultaneously. • Division - Int representing the time strategy of the midi file. We will always use Division = 96. This means 96 ticks per quarter note. • Track - [ Mevent] . This represents the music that is played. Note there is a list of Track’s each which is a list of Mevent’s (midi-event). 3/16/2016 15 Cse536 Functional Programming MIDI Events • MIDI events come in two flavors. – Normal event. NoteOn, NoteOff, or ProgChange (switch instrument) – Meta event. Change how things are played. » Of interest to us: SetTempo - change the speed of music played. 3/16/2016 16 Cse536 Functional Programming MIDI Library - MIDI Events -- Midi Events data MidiEvent = NoteOff MidiChannel MPitch Velocity | NoteOn MidiChannel MPitch Velocity | ProgChange MidiChannel ProgNum | ... deriving (Show, Eq) type MPitch = Int type Velocity = Int type ProgNum = Int type MidiChannel = Int -- Meta Events data MetaEvent = SetTempo MTempo | ... deriving (Show, Eq) type MTempo = Int 3/16/2016 17 Cse536 Functional Programming Translating performToMidi :: Performance -> MidiFile performToMidi pf = MidiFile mfType (Ticks division) (map performToMEvs (splitByInst pf)) mfType = 1 division = 96 • First, take the performance (a list of events, each of which carries information about instrument, pitch, and duration), and split it into a list of performances, each of which deals with only one instrument. – splitByInst :: Performance -> [Performance] • For each of these single-instrument performances turn it into a list of Mevent’s. – performToMEvs :: Performance -> [ Mevent] • Last, make a MidiFile data type out of it using the default MFType and Division 3/16/2016 18 Cse536 Functional Programming Side Trip - Partition partition even [1,2,3,4,6,2,45] --> ([2,4,6,2],[1,3,45]) • Partition takes a predicate and a list, and returns a pair of lists. The first element of the pair is all the elements of the list that meet the predicate. The second element all those that don’t. partition :: (a -> Bool) -> [a] -> ([a],[a]) partition p xs = foldr select ([],[]) xs where select x (ts,fs) | p x = (x:ts,fs) | otherwise = (ts, x:fs) 3/16/2016 19 Cse536 Functional Programming SplitByInst Track Instrument splitByInst :: Performance ->[(MidiChannel,ProgNum,Performance)] splitByInst = aux 0 p aux n aux n let p where [] = [] pf = i = eInst (head pf) (pf1,pf2) = partition (\e -> eInst e == i) pf n' = if n==8 then 10 else n+1 in if i==Percussion then (9, 0, pf1) : aux n pf2 else if n>15 then error "No more than 16 instruments allowed" else (n, fromEnum i, pf1) : aux n' pf2 3/16/2016 20 Cse536 Functional Programming PerformToMEvs performToMEvs :: (MidiChannel,ProgNum,Performance) -> [MEvent] performToMEvs (ch,pn,perf) = let setupInst = MidiEvent 0 (ProgChange ch pn) setTempo = MetaEvent 0 (SetTempo tempo) loop [] = [] loop (e:es) = let (mev1,mev2) = mkMEvents ch e in mev1 : insertMEvent mev2 (loop es) in setupInst : setTempo : loop perf • For each event, set up the instrument and the tempo, and generate a start and stop event. • The start event goes at the beginning of the list. But where does the stop event go? 3/16/2016 21 Cse536 Functional Programming First: insertMEvent • Since the stop event can possibly go any where in the list generated we need an function that inserts a time-stamped event in the correct location in a timestamped ordered list. insertMEvent :: MEvent -> [MEvent] -> [MEvent] insertMEvent ev1 [] = [ev1] insertMEvent ev1@(MidiEvent t1 _) evs@(ev2@(MidiEvent t2 _):evs') = if t1 <= t2 then ev1 : evs else ev2 : insertMEvent ev1 evs' 3/16/2016 22 Cse536 Functional Programming Second: mkMEvents mkMEvents :: MidiChannel -> Event -> (MEvent,MEvent) mkMEvents mChan (Event { eTime = t, ePitch = p, eDur = d }) = (MidiEvent (toDelta t) (NoteOn mChan p 127), MidiEvent (toDelta (t+d))(NoteOff mChan p 127)) toDelta t = round (t * 4.0 * intToFloat division) • Generate a NoteOn and a NoteOff for each note at the appropriate time. 3/16/2016 23 Cse536 Functional Programming Step 3: Writing a MIDI file -- outputMidiFile :: String -> MidiFile -> IO () test :: Music -> IO () test m = outputMidiFile "test.mid" (performToMidi (perform defCon m)) defCon :: Context -- Defauult Initial Context defCon = Context { cTime = 0, cInst = AcousticGrandPiano, cDur = metro 120 qn, cKey = 0 } Note it is not until we write the midi-file to disk that we move from the pure functional world, to the world of actions. 3/16/2016 24 Cse536 Functional Programming Playing Music testWin95 m = do { test m ; system "mplayer test.mid” ; return () } testNT m = do { test m ; system "mplay32 test.mid” ; return ()} testLinux m = do { test m ; system "playmidi -rf test.mid” ; return ()} 3/16/2016 25 Cse536 Functional Programming Let’s Play Some Music! cScale = line [c 4 qn, d 4 qn, e 4 qn, f 4 qn, g 4 qn, a 4 qn, b 4 qn, c 5 qn] testNT cScale 3/16/2016 26 Cse536 Functional Programming :+: E :+: :+: E :+: :+: B 3/16/2016 D D D 27