Slides

advertisement
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
Download