Выбрать главу

У нас есть аналогичные операции преобразования во времени для событий и треков, это говорит о том,

что мы можем ввести специальный класс, который объединит в себе эти операции. Назовём его классом

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 ноты для ударных обрабатываются особым

образом. Десятый канал выделен под ударные, при этом номер инструмента игнорируется, а вместо этого