композицию, для этого мы сместим второй трек на длину первого и сыграем их одновременно:
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;
Эти преобразования отвечают длительностям нот в европейской музыкальной традиции.
Громкость ноты