tfmTime = M. fromAbsTime . M. fromRealTime timeDiv .
sortBy (compare ‘on‘ fst)
В этой функции мы сначала сортируем события во времени, затем переходим от абсолютных единиц к
относительным и в самом конце производим квантование по времени. Функция sortBy сортирует элементы
согласно некоторой функции упорядочивания:
sortBy :: (a -> a -> Ordering) -> [a] -> [a]
Она принимает функцию упорядочивания и список. Мы воспользовались этой функцией, потому что нам
необходимо отсортировать элементы списка сообщений по значению временных отсчётов. Функцию упоря-
дочивания мы составляем с помощью специальной функции on, которая определена в модуле Data.Function.
С этой функцией мы уже сталкивались, когда говорили о функциях высшего порядка, она принимает функ-
цию двух аргументов и функцию одного аргумента и словно “подкладывает” вторую функцию под первую:
Prelude Data.Function> :t on
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
Теперь напишем функцию mergeInstr. Она устанавливает инструменты на каналы и преобразует события
в последовательность midi-сообщений. При этом мы различаем сообщения для ударных и сообщения для всех
остальных инструментов:
312 | Глава 21: Музыкальный пример
mergeInstr :: ([[MidiEvent]], [MidiEvent]) -> M.Track Double
mergeInstr (instrs, drums) = concat $ drums’ : instrs’
where instrs’ = zipWith setChannel ([0 .. 8] ++ [10 .. 15]) instrs
drums’
= setDrumChannel drums
setChannel :: M.Channel -> [MidiEvent] -> M.Track Double
setChannel = undefined
setDrumChannel :: [MidiEvent] -> M.Track Double
setDrumChannel =
undefined
Имя instrs’ указывает на последовательность списков сообщений для каждого неударного инструмента.
Функция setChannel принимает номер канала и список событий. По ним она строит список midi-сообщений.
Определим эту функцию:
setChannel :: M.Channel -> [MidiEvent] -> M.Track Double
setChannel ch ms = case ms of
[]
-> []
x:xs
-> (0, M.ProgramChange ch (instrId x)) : (fromEvent ch =<< ms)
instrId = noteInstr . eventContent
fromEvent :: M.Channel -> MidiEvent -> M.Track Double
fromEvent = undefined
Первым событием мы присоединяем событие, которое устанавливает на данном канале определённый
инструмент. По построению программы все ноты в переданном списке играются на одном и том же инстру-
менте, поэтому мы узнаём идентификатор инструмента из первого элемента списка. У нас появилась новая
неопределённая функция fromEvent она переводит сообщение в список midi-сообщений:
fromEvent :: M.Channel -> MidiEvent -> M.Track Double
fromEvent ch e = [
(eventStart e, noteOn n),
(eventStart e + eventDur e, noteOff n)]
where n = clipToMidi $ eventContent e
noteOn
n = M.NoteOn
ch (notePitch n) (noteVolume n)
noteOff n = M.NoteOff ch (notePitch n) 0
clipToMidi :: Note -> Note
clipToMidi n = n {
notePitch
= clip $ notePitch n,
noteVolume
= clip $ noteVolume n }
where clip = max 0 . min 127
Определив эти функции, мы легко можем написать и функцию setDrumChannel она переводит сообщения
для ударных инструментов в midi-сообщения:
setDrumChannel :: [MidiEvent] -> M.Track Double
setDrumChannel ms = fromEvent drumChannel =<< ms
where drumChannel = 9
Для ударных инструментов выделен отдельный канал. Считается, что все они происходят на 10 канале.
Поскольку в библиотеке HCodecs первый канал называется нулевым, мы будем записывать все сообщения на
девятый канал.
Мы переводим событие в два midi-сообщения, первое говорит о том, что мы начали играть ноту, а второе
говорит о том, что мы закончили её играть. Функция clipToMidi приводит значения для высоты и громкости
в диапазон midi.
Нам осталось определить только одну функцию. Эта функция распределяет события по инструментам.
Сначала мы разделим события на те, что играются на ударных и неударных инструментах, а затем разделим
“неударные” ноты по инструментам:
import Control.Arrow(first, second)
import Data.List(sortBy, groupBy, partition)
...
groupInstr :: Score -> ([[MidiEvent]], [MidiEvent])
Перевод в midi | 313
groupInstr = first groupByInstrId .
partition (not . isDrum . eventContent) . trackEvents
where groupByInstrId = groupBy ((==) ‘on‘ instrId) .
sortBy
(compare ‘on‘ instrId)
В этом определении мы воспользовались двумя новыми стандартными функциями из модуля Data.List.
Функция partition разделяет список на пару списков. В первом списке находятся все те элементы, для
которых заданный предикат вернул True, а во втором списке – все остальные элементы исходного списка:
Prelude Data.List> :t partition
partition :: (a -> Bool) -> [a] -> ([a], [a])
Функция groupBy превращает список в список списков:
Prelude Data.List> :t groupBy
groupBy :: (a -> a -> Bool) -> [a] -> [[a]]