haskell-notes

можем перенести и на значения типа 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 }

Читай продолжение на следующей странице
Добавить комментарии