tfmTime = M.
fromAbsTime . M. fromRealTime timeDiv .sortBy (compare ‘on‘ fst)
В этой функции мы сначала сортируем события во времени, затем переходим от абсолютных единиц к
относительным и в самом конце производим квантование по времени. Функция sortBy сортирует элементы
согласно некоторой функции упорядочивания:
sortBy ::
(a -> a -> Ordering) -> [a] -> [a]Она принимает функцию упорядочивания и список. Мы воспользовались этой функцией, потому что нам
необходимо отсортировать элементы списка сообщений по значению временных отсчётов. Функцию упоря-
дочивания мы составляем с помощью специальной функции on, которая определена в модуле Data.Function
.С этой функцией мы уже сталкивались, когда говорили о функциях высшего порядка, она принимает функ-
цию двух аргументов и функцию одного аргумента и словно “подкладывает” вторую функцию под первую:
Prelude Data.Function> :
t onon ::
(b -> b -> c) -> (a -> b) -> a -> a -> cТеперь напишем функцию mergeInstr. Она устанавливает инструменты на каналы и преобразует события
в последовательность midi-сообщений. При этом мы различаем сообщения для ударных и сообщения для всех
остальных инструментов:
312 | Глава 21: Музыкальный пример
mergeInstr ::
([[MidiEvent]], [MidiEvent]) -> M.Track DoublemergeInstr (instrs, drums) =
concat $ drums’ : instrs’where
instrs’ = zipWith setChannel ([0 .. 8] ++ [10 .. 15]) instrsdrums’
=
setDrumChannel drumssetChannel :: M.Channel ->
[MidiEvent] -> M.Track DoublesetChannel =
undefinedsetDrumChannel ::
[MidiEvent] -> M.Track DoublesetDrumChannel =
undefined
Имя instrs’ указывает на последовательность списков сообщений для каждого неударного инструмента.
Функция setChannel принимает номер канала и список событий. По ним она строит список midi-сообщений.
Определим эту функцию:
setChannel :: M.Channel ->
[MidiEvent] -> M.Track DoublesetChannel ch ms = case
ms of[]
-> []
x:
xs->
(0, M.ProgramChange ch (instrId x)) : (fromEvent ch =<< ms)instrId =
noteInstr . eventContentfromEvent :: 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 enoteOn
n = M.NoteOn
ch (notePitch n) (noteVolume n)
noteOff n = M.NoteOff
ch (notePitch n) 0clipToMidi :: 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 DoublesetDrumChannel ms =
fromEvent drumChannel =<< mswhere
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) . trackEventswhere
groupByInstrId = groupBy ((==) ‘on‘ instrId) .sortBy
(compare ‘on‘ instrId)
В этом определении мы воспользовались двумя новыми стандартными функциями из модуля Data.List
.Функция partition разделяет список на пару списков. В первом списке находятся все те элементы, для
которых заданный предикат вернул True
, а во втором списке – все остальные элементы исходного списка:Prelude Data.List> :
t partitionpartition ::
(a -> Bool) -> [a] -> ([a], [a])Функция groupBy превращает список в список списков:
Prelude Data.List> :
t groupBygroupBy ::
(a -> a -> Bool) -> [a] -> [[a]]