haskell-notes

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, мы можем написать мело-

дию. Ниже приведён небольшой пример. Инструменты:

Страницы: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162