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]]
Если бинарная функция на соседних элементах исходного списка вернула True, то они помещаются в
один подсписок. Эта функция используется для того чтобы сгруппировать элементы списка по какому-нибудь
признаку. При этом для того чтобы сгруппировать элементы по идентификатору инструмента, мы сначала
отсортировали события по значению идентификатора. После этого значения с одинаковыми идентификато-
рами стали соседними и мы сгруппировали их с помощью groupBy.
Функция first применяет функцию к первому элементу пары. Вот мы и закончили, можно послушать ре-
зультаты. На самом деле остались два нюанса. В функции setChannel мы полагаем, что мелодия начинается
в момент времени t = 0, но на практике это может оказаться не так, мы можем сместить ноты функцией
delay в отрицательную сторону. Тогда первые ноты будут содержать отрицательное время начала события.
Но мы можем исправить эту ситуацию, сместив все ноты на время самой первой ноты, конечно смещать
необходимо только в том случае если время окажется отрицательным:
alignEvents :: [MidiEvent] -> [MidiEvent]
alignEvents es
| d < 0
= map (delay (abs d)) es
| otherwise = es
where d = minimum $ map eventStart es
Вызовем эту функцию сразу после функции trackEvents в функции groupInstr. Второй нюанс заключа-
ется в том, что каждый трек в midi-файле должен заканчиваться специальным сообщением, в библиотеке
HCodecs оно обозначается с помощью конструктора TrackEnd. В самом конце необходимо добавить сообще-
ние (0, TrackEnd):
toTrack :: Score -> M.Track M.Ticks
toTrack = addEndMsg . tfmTime . mergeInstr . groupInstr
addEndMsg :: M.Track M.Ticks -> M.Track M.Ticks
addEndMsg = (++ [(0, M.TrackEnd)])
Теперь мы можем проверить, что у нас получилось. Создадим файл:
module Main where
import System
import Track
import Score
import Codec.Midi
out = (>> system ”timidity tmp.mid”) .
exportFile ”tmp.mid” . render
В функции out мы переводим нотную запись в значение типа Midi, затем сохраняем это значение в файле
tmp. mid и в самом конце запускаем файл с помощью проигрывателя timidity. Вместо timidity вы можете
воспользоваться вашим любимым проигрывателем midi-файлов. Теперь загрузим модуль Main в интерпре-
татор. Послушаем ноту до:
*Main> out c
314 | Глава 21: Музыкальный пример
Далее следуют сообщения из проигрывателя timidity и долгожданный звук. Мы слышим ноту до, сыг-
ранную на рояле. Наберём какую-нибудь мелодию:
*Main> let x = line [c, hn e, hn e, low b, c]
*Main> out x
Сыграем в два раза быстрее, на другом инструменте:
*Main> out $ instr 15 $ hn x
Сыграем канон. Канон это когда одна и та же мелодия ведётся в разных голосах с запаздыванием. Сыграем
двухголосный канон:
*Main> out $ instr 80 (loop 3 x) =:= delay 2 (instr 65 $ low $ loop 3 x)
Номера инструментов можно посмотреть по справке к протоколу General Midi. Это дополнение к прото-
колу midi определяет какие номера каким инструментам должны соответствовать. Звучит ужасно, но звучит!
21.5 Пример
Опираясь на примитивы композиции, которые мы определил в модуле Score, мы можем написать мело-
дию. Ниже приведён небольшой пример. Инструменты: