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 = casems of
[]
-> []
x :xs
->(0, M.ProgramChangech (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)]
wheren =clipToMidi $eventContent e
noteOn
n = M.NoteOn
ch (notePitch n) (noteVolume n)
noteOff n = M.NoteOffch (notePitch n) 0
clipToMidi :: Note -> Note
clipToMidi n =n {
notePitch
=clip $notePitch n,
noteVolume
=clip $noteVolume n }
whereclip =max 0 .min 127
Определив эти функции, мы легко можем написать и функцию setDrumChannel она переводит сообщения
для ударных инструментов в midi-сообщения:
setDrumChannel ::[ MidiEvent] -> M.Track Double
setDrumChannel ms =fromEvent drumChannel =<<ms
wheredrumChannel =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
wheregroupByInstrId =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]]
Если бинарная функция на соседних элементах исходного списка вернула True, то они помещаются в
один подсписок. Эта функция используется для того чтобы сгруппировать элементы списка по какому-нибудь
признаку. При этом для того чтобы сгруппировать элементы по идентификатору инструмента, мы сначала
отсортировали события по значению идентификатора. После этого значения с одинаковыми идентификато-
рами стали соседними и мы сгруппировали их с помощью groupBy.
Функция first применяет функцию к первому элементу пары. Вот мы и закончили, можно послушать ре-
зультаты. На самом деле остались два нюанса. В функции setChannel мы полагаем, что мелодия начинается
в момент времени t =0, но на практике это может оказаться не так, мы можем сместить ноты функцией
delay в отрицательную сторону. Тогда первые ноты будут содержать отрицательное время начала события.
Читать дальше