можем перенести и на значения типа Track.
delayTrack :: Num t => t -> Track t a -> Track t a
delayTrack d (Track t es) = Track (t + d) (map (delayEvent d) es)
stretchTrack :: Num t => t -> Track t a -> Track t a
stretchTrack s (Track t es) = Track (t * s) (map (stretchEvent s) es)
Класс преобразований во времени
У нас есть аналогичные операции преобразования во времени для событий и треков, это говорит о том,
что мы можем ввести специальный класс, который объединит в себе эти операции. Назовём его классом
Temporal (временн ой):
class Temporal a where
type Dur a :: *
dur
:: a -> Dur a
delay
:: Dur a -> a -> a
stretch :: Dur a -> a -> a
В этом классе определён один тип, который обозначает размерность времени, и три метода в дополнении
к методам delay и stretch мы добавим метод dur, мы будем считать, что всё что происходит во времени
конечно и с помощью метода dur мы всегда можем узнать протяжённость значения их класса Temporal во
времени. Для определения этого класса нам придётся подключить расширение TypeFamilies. Теперь мы
легко можем определить экземпляры класса Temporal для Event и Track:
instance Num t => Temporal (Event t a) where
type Dur (Event t a) = t
dur
= eventDur
delay
= delayEvent
stretch = stretchEvent
instance Num t => Temporal (Track t a) where
type Dur (Track t a) = t
dur
= trackDur
delay
= delayTrack
stretch = stretchTrack
Композиция треков
Определим две полезные в музыке операции: параллельную и последовательную композицию треков. В
параллельной композиции мы играем два трека одновременно:
(=:=) :: Ord t => Track t a -> Track t a -> Track t a
Track t es =:= Track t’ es’ = Track (max t t’) (es ++ es’)
Теперь общая длительность трека равна длительности большего из треков, а события включают в себя
события каждого из треков. С помощью преобразований во времени мы можем определить последовательную
композицию, для этого мы сместим второй трек на длину первого и сыграем их одновременно:
308 | Глава 21: Музыкальный пример
(+:+) :: (Ord t, Num t) => Track t a -> Track t a -> Track t a
(+:+) a b = a =:= delay (dur a) b
При этом у нас как раз и получится, что мы сначала сыграем целиком трек a, а затем трек b. Теперь
определим аналоги операций =:= и +:+ для списков:
chord :: (Num t, Ord t) => [Track t a] -> Track t a
chord = foldr (=:=) (silence 0)
line :: (Num t, Ord t) => [Track t a] -> Track t a
line = foldr (+:+) (silence 0)
Мы можем определить в терминах этих операций цикличный повтор событий:
loop :: (Num t, Ord t) => Int -> Track t a -> Track t a
loop n t = line $ replicate n t
Экземпляры стандартных классов
Мы можем сделать тип трек экземпляром класса Functor:
instance Functor (Event t) where
fmap f e = e{ eventContent = f (eventContent e) }
instance Functor (Track t) where
fmap f t = t{ trackEvents = fmap (fmap f) (trackEvents t) }
Мы можем также определить экземпляр для класса Monoid. Параллельная композиция будет операцией
объединения, а нейтральным элементом будет тишина, которая длится ноль единиц времени:
instance (Ord t, Num t) => Monoid (Track t a) where
mappend = (=:=)
mempty
= silence 0
21.3 Ноты в midi
С помощью типа Track мы можем описывать всё, что имеет свойство случаться во времени и длиться,
мы можем описывать наборы событий. Операции из класса Temporal и операции последовательной и парал-
лельной композиции дают нам возможность собирать сложные наборы событий из простейших. Но для того
чтобы это стало музыкой, нам не хватает нот.
Так построим их. Поскольку мы собираемся играть музыку в midi, наши ноты будут содержать только три
основных параметра, это номер инструмента, громкость и высота. Длительность ноты будет кодироваться в
событии, эта информация уже встроена в тип Track.
data Note = Note {
noteInstr
:: Instr,
noteVolume
:: Volume,
notePitch
:: Pitch,
isDrum
:: Bool
}
Итак нота содержит код инструмента, громкость и высоту и ещё один параметр. По последнему пара-
метру можно узнать сыграна нота на барабане или нет. В midi ноты для ударных обрабатываются особым
образом. Десятый канал выделен под ударные, при этом номер инструмента игнорируется, а вместо этого
высота звука кодирует номер ударного инструмента. Теперь определимся с типами параметров:
type Instr
= Int
type Volume = Int
type Pitch
= Int
Целые числа соответствуют целым числам в протоколе midi. Значения для типов Volume и Pitch лежат в
диапазоне от 0 до 127.
Введём специальное обозначение для музыкального типа Track:
type Score = Track Double Note
Ноты в midi | 309
Синонимы для нот
Высота ноты
Музыкантам ближе буквенные обозначения для нот нежели коды midi. Определим удобные синонимы:
note :: Int -> Score
note n = Track 1 [Event 0 1 (Note 0 64 (60+n) False)]
Эта функция строит трек, который содержит одну ноту. Нота длится одну целую длительность играется
на инструменте с кодом 0, на средней громкости. Параметр функции задаёт смещение от ноты до первой
октавы. Определим остальные ноты:
a, b, c, d, e, f, g,
as, bs, cs, ds, es, fs, gs,
af, bf, cf, df, ef, ff, gf :: Score
c = note 0;
cs = note 1;
d = note 2;
ds = note 3;
…
Первая буква содержит буквенное обозначение ноты, а вторая либо s (от англ. sharp диез) или f (от англ.
flat бемоль). Все эти ноты находятся в первой октаве, но смещением высоты на 12 единиц мы легко можем
смещать эти ноты в любую другую октаву:
higher :: Int -> Score -> Score
higher n = fmap (a -> a{ notePitch = 12*n + notePitch a })
lower :: Int -> Score -> Score
lower n = higher (—n)
high :: Score -> Score
high = higher 1
low :: Score -> Score
low = lower 1
С помощью этих функций мы легко можем смещать группы нот в любую октаву. Функция higher прини-
мает число октав, на которые необходимо сместить вверх высоту во всех нотах трека. Смещение высоты на
12 определяет смещение на одну октаву. Остальные функции определены в через функцию higher.
Длительность ноты
Пока что наши ноты длятся 1 единицу времени. Но нам бы хотелось иметь в распоряжении и другие дли-
тельности. Ноты других длительностей мы можем легко получать с помощью функции stretch, мы просто
изменим масштаб времени и длительность всех нот изменится. Определим несколько синонимов:
bn, hn, qn, en, sn :: Score -> Score
— (brewis note)
(half note)
(quater note)
bn = stretch 2;
hn = stretch 0.5;
qn = stretch 0.25;
— (eighth note)
(sizth note)
en = stretch 0.125;
sn = stretch 0.0625;
Эти преобразования отвечают длительностям нот в европейской музыкальной традиции.
Громкость ноты
Пока мы умеем создавать ноты средней громкости, но мы можем определить преобразователи на манер
тех, что изменяли высоту звука октавами:
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 }





