Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
250 lines (196 sloc) 9.43 KB
FromMidi2: an alternative Midi-to-Music conversion algorithm.
Author: Donya Quick
Last modified: 28-Dec-2016
The goal of this module is to provide a more intelligent
parse from MIDI files to Music structures. The fromMidi
function will convert Midi into Music, but the resulting
structure is one big parallel composition with no other
relationships between the notes. The fromMidi2 function
here is an attempt to provide a parse of musical features
that is more in line with how a human might write them
in Euterpea or perceive them by ear. It works best on
MIDI files that are very close to paper score in terms
of how the events are structured. The functions here are
not intended for use with "messy" MIDI files that have
been recorded from a live performance without quantization.
You can use fromMidi2 as an alternative to fromMidi to
parse a Midi value into a Music value with a better
method of grouping events together. The same algorithm
can be applied directly to a Music value with the
restructure function.
Examples of how to use fromMidi2 and restructure:
testMidi file = do
x <- importFile file
case x of Left err -> error err
Right m -> do
let v = fromMidi2 m
putStrLn $ show v
play v
myMusic :: Music (Pitch, Volume)
myMusic = ...
newMusic :: Music (Pitch, Volume)
newMusic = restructure myMusic
Restructuring is done from the MEvent level. Importantly,
this means that there are no tempo changes or other Modify
nodes in the resulting Music value! A global tempo of
120BPM is assumed. If your MIDI file has a different BPM,
you can use fromMidi in combination with restructure and
then apply a tempo modifier afterwards.
The method for organizing events is:
(1) Identify and group chords where every note
has the same start time and duration.
(2) Identify and group sequential patterns where items
are back-to-back. Note that this may include a mix of
single notes and chords from step 1.
(3) Greedily group any patterns with gaps between
them into a sequence with rests.
> module Euterpea.IO.MIDI.FromMidi2 (fromMidi2, restructure, Chunk, chunkEvents, chunkToMusic)where
> import Euterpea.Music hiding (E)
> import Euterpea.IO.MIDI.ToMidi
> import Euterpea.IO.MIDI.GeneralMidi
> import Euterpea.IO.MIDI.MEvent
> import Euterpea.IO.MIDI.FromMidi
> import Data.List
> import Codec.Midi
The primary exported functions for this module are:
> fromMidi2 :: Midi -> Music (Pitch, Volume)
> fromMidi2 = restructure . fromMidi
> restructure :: (ToMusic1 a) => Music a -> Music (Pitch, Volume)
> restructure = parseFeaturesI
Other exported features are related to the Chunk datatype.
A Chunk is the data structure used to group events by the algorithm
described at the top of this file. Par and Chord correspond to features
that will be composed in parallel (:=:) at different levels, and Seq
corresponds to features that will be composed in sequence (:+:). E is
a wrapper for single events and R is a rest place-holder.
> type Onset = Dur -- to clarify some type signatures
> data Chunk = Par [Chunk] | Seq [Chunk] | Chord [Chunk] | E MEvent | R Onset Dur
> deriving Eq
Initially, each MEvent is placed in its own chunk.
> initChunk :: [MEvent] -> [Chunk]
> initChunk mevs =
> let mevs' = sortBy sortFun mevs
> in map E mevs'
The chunkChord function looks for chunks that share the same
onset and duration and places them together in Chord chunks.
> chunkChord :: [Chunk] -> [Chunk]
> chunkChord [] = []
> chunkChord (c:cs) =
> let cChord = filter (chordWith c) cs
> notInChord = filter (\v -> not $ elem v cChord) cs
> in if null cChord then c : chunkChord cs
> else Chord (c:cChord) : chunkChord notInChord
> chordWith :: Chunk -> Chunk -> Bool
> chordWith c0 c = chunkOnset c == chunkOnset c0 && chunkDur c == chunkDur c0
The chunkMel function looks for sequences of chunks (which need
not be adjacent in the input list) where the end time of one chunk
is equal to the start time of the next chunk. There are no gaps
permitted, so notes separated by rests will not be grouped here.
> chunkMel :: [Chunk] -> [Chunk]
> chunkMel [] = []
> chunkMel x@(c:cs) =
> let cMel = buildMelFrom (chunkOnset c) x -- get ALL possible melody elements
> notInMel = filter (\v -> not $ elem v cMel) x
> in if null cMel then c : chunkMel cs
> else Seq cMel : chunkMel notInMel
> buildMelFrom :: Onset -> [Chunk] -> [Chunk]
> buildMelFrom t [] = []
> buildMelFrom t (c:cs) =
> if chunkOnset c == t then c : buildMelFrom (t + chunkDur c) cs
> else buildMelFrom t cs
The chunkSeqs function is more general and will look for anything
that can be grouped together linearly in time, even if it requires
inserting a rest. This will group together all non-overlapping
chunks in a greedy fashion.
> chunkSeqs :: [Chunk] -> [Chunk]
> chunkSeqs [] = []
> chunkSeqs x@(c:cs) =
> let s = seqWithRests (chunkOnset c) x
> notInS = filter (\v -> not $ elem v s) x
> in if s == [c] then c : chunkSeqs cs
> else Seq s : chunkSeqs notInS
> seqWithRests :: Onset -> [Chunk] -> [Chunk]
> seqWithRests t [] = []
> seqWithRests t x@(c:cs) =
> let tc = chunkOnset c
> dt = tc - t
> in if dt == 0 then c : seqWithRests (tc + chunkDur c) cs
> else if dt > 0 then R t dt : c : seqWithRests (tc + chunkDur c) cs
> else seqWithRests t cs
Finally, chunkEvents combines all of these methods in a particular
order that establishes preference for chords first, then melodies
(which may include chords), and then sequences including rests.
Anything left over will be handled by an outer Par.
> chunkEvents :: [MEvent] -> Chunk
> chunkEvents = Par . chunkSeqs . chunkMel . chunkChord. initChunk
Chunks can be converted directly to Music. Durations have to be
divided in half because MEvents deal with seconds, while Music
deals with duration as whole notes (1 whole note = 2 seconds).
> chunkToMusic :: Chunk -> Music (Pitch, Volume)
> chunkToMusic (E e) = note (eDur e / 2) (pitch $ ePitch e, eVol e)
> chunkToMusic (R o d) = rest (d/2)
> chunkToMusic (Seq x) = line(map chunkToMusic x)
> chunkToMusic (Chord x) = chord(map chunkToMusic x)
> chunkToMusic (Par x) = chord $ map (\v -> rest (chunkOnset v / 2) :+: chunkToMusic v) x
The parseFeatures function will take an existing Music value, such
as one returned by fromMidi, and use the algorithms above to identify
musical features (chords and melodies) and construct a new Music
tree that is performance-equivalent to the original.
> parseFeatures :: (ToMusic1 a) => Music a -> Music (Pitch, Volume)
> parseFeatures = removeZeros . chunkToMusic . chunkEvents . perform
> parseFeaturesI :: (ToMusic1 a) => Music a -> Music (Pitch, Volume)
> parseFeaturesI m =
> let mevs = perform m
> (iList, mevsI) = unzip $ splitByInst mevs
> parsesI = map (removeZeros . chunkToMusic . chunkEvents) mevsI
> in chord $ zipWith instrument iList parsesI
================
Utility Functions and Type Class Instances
First, some functions to pretty-up printing of things for debugging purposes
> doubleShow :: Rational -> String
> doubleShow x = show (fromRational x :: Double)
> pcShow :: AbsPitch -> String
> pcShow = show . fst . pitch
> listShow, listShowN :: (Show a) => [a] -> String
> listShow x = "["++(concat $ intersperse ", " $ map show x)++"]"
> listShowN x = "[\n "++(concat $ intersperse ",\n " $ map show x)++"\n]"
> listShowX :: (Show a) => Int -> [a] -> String
> listShowX i x = let v = concat (take i (repeat " ")) in
> "[\n"++v++(concat $ intersperse (",\n"++v) $ map show x)++"\n"++v++"]"
> instance Show Chunk where
> show (E e) = "E "++doubleShow (eTime e)++" "++pcShow (ePitch e)++" "++doubleShow (eDur e)
> show s@(Seq x) = "S "++doubleShow (chunkOnset s)++" "++listShowX 4 x
> show c@(Chord x) = "C "++doubleShow (chunkOnset c)++" "++listShowX 6 x
> show p@(Par x) = "P "++doubleShow (chunkOnset p)++" "++listShowX 2 x
> show (R o d) = "R "++doubleShow o++" "++doubleShow d
An Ord instance for Chunk that enforces sorting based on onset time. No
other features are considered.
> instance Ord Chunk where
> compare x1 x2 = compare (chunkOnset x1) (chunkOnset x2)
Functions to determine the start time (onset) and duration of a Chunk.
> chunkOnset :: Chunk -> Onset
> chunkOnset (Seq x) = if null x then error "Empty Seq!" else chunkOnset (head x)
> chunkOnset (Chord x) = if null x then error "Empty Chord!" else chunkOnset (head x)
> chunkOnset (Par x) = if null x then 0 else minimum $ map chunkOnset x
> chunkOnset (E e) = eTime e
> chunkOnset (R o d) = o
> chunkEnd :: Chunk -> Onset
> chunkEnd (Seq x) = if null x then error "Empty Seq!" else chunkEnd (last x)
> chunkEnd (Chord x) = if null x then error "Empty Chord!" else chunkEnd (head x)
> chunkEnd (Par x) = if null x then 0 else maximum $ map chunkEnd x
> chunkEnd (E e) = eTime e + eDur e
> chunkEnd (R o d) = o + d
> chunkDur :: Chunk -> Dur
> chunkDur (Seq x) = if null x then error "Empty Seq!" else sum $ map chunkDur x
> chunkDur (Chord x) = if null x then error "Empty Chord!" else chunkDur (head x)
> chunkDur c@(Par x) = if null x then 0 else
> let o = chunkOnset c
> e = chunkEnd c
> in e-o
> chunkDur (E e) = eDur e
> chunkDur (R o d) = d
Special sorting function for MEvents.
> sortFun :: MEvent -> MEvent -> Ordering
> sortFun e1 e2 =
> if eTime e1 == eTime e2 then compare (ePitch e1) (ePitch e2)
> else compare (eTime e1) (eTime e2)