Читаем Учебник по Haskell полностью

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

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]]

Перейти на страницу:

Похожие книги

C++: базовый курс
C++: базовый курс

В этой книге описаны все основные средства языка С++ - от элементарных понятий до супервозможностей. После рассмотрения основ программирования на C++ (переменных, операторов, инструкций управления, функций, классов и объектов) читатель освоит такие более сложные средства языка, как механизм обработки исключительных ситуаций (исключений), шаблоны, пространства имен, динамическая идентификация типов, стандартная библиотека шаблонов (STL), а также познакомится с расширенным набором ключевых слов, используемым в .NET-программировании. Автор справочника - общепризнанный авторитет в области программирования на языках C и C++, Java и C# - включил в текст своей книги и советы программистам, которые позволят повысить эффективность их работы. Книга рассчитана на широкий круг читателей, желающих изучить язык программирования С++.

Герберт Шилдт

Программирование, программы, базы данных