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

Пока мы умеем создавать ноты средней громкости, но мы можем определить преобразователи на манер

тех, что изменяли высоту звука октавами:

louder :: Int -> Score -> Score

louder n = fmap $ \a -> a{ noteVolume = n + noteVolume a }

quieter :: Int -> Score -> Score

quieter n = louder (-n)

310 | Глава 21: Музыкальный пример

Смена инструмента

Изначально мы создаём ноты, которые играются на инструменте с кодом 0, в протоколе General Midi этот

номер соответствует роялю. Но с помощью класса Functor мы легко можем изменить инструмент:

instr :: Int -> Score -> Score

instr n = fmap $ \a -> a{ noteInstr = n, isDrum = False }

drum :: Int -> Score -> Score

drum n = fmap $ \a -> a{ notePitch = n, isDrum = True }

Согласно протоколу midi в случае ударных инструментов высота звука кодирует инструмент. Поэтому

в функции drum мы изменяем именно поле notePitch. Создадим также несколько синонимов для создания

нот, которые играются на барабанах. В этом случае нам не важна высота звука но важна громкость:

bam :: Int -> Score

bam n = Track 1 [Event 0 1 (Note 0 n 35 True)]

Номер 35 кодирует “бочку”.

Паузы

Слово silence верно отражает смысл, но оно слишком длинное. Давайте определим несколько синони-

мов:

rest :: Double -> Score

rest = silence

wnr = rest 1;

bnr = bn wnr;

hnr = hn wnr;

qnr = qn wnr;

enr = en wnr;

snr = sn wnr;

21.4 Перевод в midi

Теперь мы можем составить какую нибудь мелодию:

q = line [c, c, hn e, hn d, bn e, chord [c, e]]

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

функцию:

render :: Score -> Midi

Мы реализуем простейший случай. Будем считать, что у нас только 15 инструментов, а все остальные

инструменты – ударные. Мы запишем нашу музыку на один трек midi-файла, распределив 15 неударных

инструментов по разным каналам. Ещё одно упрощение заключается в том, что мы зададим фиксированное

разрешение по времени для всех возможных мелодий. Будем считать, что 96 ударов для одной четверти нам

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

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

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

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

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

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

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

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