haskell-notes

import qualified Codec.Midi as M

render :: Score -> Midi

render s = M.Midi M.SingleTrack (M.TicksPerBeat divisions) [toTrack s]

divisions :: M.Ticks

divisions = 96

toTrack :: Score -> M.Track

toTrack = undefined

Мы загрузили модуль Codec.Midi под псевдонимом M, так мы сможем отличать низкоуровневые опре-

деления от тех, что мы определили сами. Теперь перед каждым именем из модуля Codec.Midi необходимо

писать приставку M.

В нашей упрощённой реализации на одном канале может играть только один инструмент. В самом начале

мы назначим инструмент на канал с помощью сообщения ProgramChange. Для этого нам необходимо понять

какому инструменту какой канал соответствует. В библиотеке HCodecs каналы идут от нуля до 15. Девятый

канал предназначен для ударных. Представим, что у нас есть функция, которая распределяет нотную запись

по инструментам:

Перевод в midi | 311

type MidiEvent = Event Double Note

groupInstr :: Score -> ([[MidiEvent]], [MidiEvent])

Эта функция принимает нотную запись, а возвращает пару. Первый элемент содержит список списков нот

для неударных инструментов, каждый подсписок содержит ноты только для одного инструмента. Второй

элемент пары содержит все ноты для ударных инструментов. Представим также, что у нас есть функция,

которая превращает эту пару в набор midi-сообщений:

mergeInstr :: ([[MidiEvent]], [MidiEvent]) -> M.Track Double

Наши отсчёты времени записаны в виде значений типа Double, Нам необходимо перейти к целочислен-

ным Ticks. Представим, что такая функция у нас уже есть:

tfmTime :: M.Track Double -> M.Track M.Ticks

Тогда функция toTrack примет вид:

toTrack :: Score -> M.Track M.Ticks

toTrack = tfmTime . mergeInstr . groupInstr

Все три составляющие функции пока не определены. Начнём с функции tfmTime. Нам необходимо от-

сортировать события во времени для того, чтобы мы смогли перейти из абсолютных отсчётов во времени в

относительные. Специально для этого в библиотеке odecs определена функция:

fromAbsTime :: Num a -> Track a -> Track a

Также нам понадобится функция:

type Time = Double

fromRealTime :: TimeDiv -> Trrack Time -> Track Ticks

Она проводит квантование во времени. С помощью неё мы преобразуем отсчёты в Double в целочисленные

отсчёты. С помощью этих функций мы можем определить функцию timeDiv так:

import Data.List(sortBy)

import Data.Function (on)

tfmTime :: M.Track Double -> M.Track M.Ticks

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

Страницы: 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