haskell-notes

Мы переводим событие в два midi-сообщения, первое говорит о том, что мы начали играть ноту, а второе

говорит о том, что мы закончили её играть. Функция clipToMidi приводит значения для высоты и громкости

в диапазон midi.

Нам осталось определить только одну функцию. Эта функция распределяет события по инструментам.

Сначала мы разделим события на те, что играются на ударных и неударных инструментах, а затем разделим

“неударные” ноты по инструментам:

import Control.Arrow(first, second)

import Data.List(sortBy, groupBy, partition)

groupInstr :: Score -> ([[MidiEvent]], [MidiEvent])

Перевод в midi | 313

groupInstr = first groupByInstrId .

partition (not . isDrum . eventContent) . trackEvents

where groupByInstrId = groupBy ((==) ‘on‘ instrId) .

sortBy

(compare ‘on‘ instrId)

В этом определении мы воспользовались двумя новыми стандартными функциями из модуля Data.List.

Функция partition разделяет список на пару списков. В первом списке находятся все те элементы, для

которых заданный предикат вернул True, а во втором списке – все остальные элементы исходного списка:

Prelude Data.List> :t partition

partition :: (a -> Bool) -> [a] -> ([a], [a])

Функция groupBy превращает список в список списков:

Prelude Data.List> :t groupBy

groupBy :: (a -> a -> Bool) -> [a] -> [[a]]

Если бинарная функция на соседних элементах исходного списка вернула True, то они помещаются в

один подсписок. Эта функция используется для того чтобы сгруппировать элементы списка по какому-нибудь

признаку. При этом для того чтобы сгруппировать элементы по идентификатору инструмента, мы сначала

отсортировали события по значению идентификатора. После этого значения с одинаковыми идентификато-

рами стали соседними и мы сгруппировали их с помощью groupBy.

Функция first применяет функцию к первому элементу пары. Вот мы и закончили, можно послушать ре-

зультаты. На самом деле остались два нюанса. В функции setChannel мы полагаем, что мелодия начинается

в момент времени t = 0, но на практике это может оказаться не так, мы можем сместить ноты функцией

delay в отрицательную сторону. Тогда первые ноты будут содержать отрицательное время начала события.

Но мы можем исправить эту ситуацию, сместив все ноты на время самой первой ноты, конечно смещать

необходимо только в том случае если время окажется отрицательным:

alignEvents :: [MidiEvent] -> [MidiEvent]

alignEvents es

| d < 0

= map (delay (abs d)) es

| otherwise = es

where d = minimum $ map eventStart es

Вызовем эту функцию сразу после функции trackEvents в функции groupInstr. Второй нюанс заключа-

ется в том, что каждый трек в midi-файле должен заканчиваться специальным сообщением, в библиотеке

HCodecs оно обозначается с помощью конструктора TrackEnd. В самом конце необходимо добавить сообще-

ние (0, TrackEnd):

toTrack :: Score -> M.Track M.Ticks

toTrack = addEndMsg . tfmTime . mergeInstr . groupInstr

addEndMsg :: M.Track M.Ticks -> M.Track M.Ticks

addEndMsg = (++ [(0, M.TrackEnd)])

Теперь мы можем проверить, что у нас получилось. Создадим файл:

module Main where

import System

import Track

import Score

import Codec.Midi

out = (>> system ”timidity tmp.mid”) .

exportFile ”tmp.mid” . render

В функции out мы переводим нотную запись в значение типа Midi, затем сохраняем это значение в файле

tmp. mid и в самом конце запускаем файл с помощью проигрывателя timidity. Вместо timidity вы можете

воспользоваться вашим любимым проигрывателем midi-файлов. Теперь загрузим модуль Main в интерпре-

татор. Послушаем ноту до:

*Main> out c

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

Далее следуют сообщения из проигрывателя timidity и долгожданный звук. Мы слышим ноту до, сыг-

ранную на рояле. Наберём какую-нибудь мелодию:

*Main> let x = line [c, hn e, hn e, low b, c]

*Main> out x

Сыграем в два раза быстрее, на другом инструменте:

*Main> out $ instr 15 $ hn x

Сыграем канон. Канон это когда одна и та же мелодия ведётся в разных голосах с запаздыванием. Сыграем

двухголосный канон:

*Main> out $ instr 80 (loop 3 x) =:= delay 2 (instr 65 $ low $ loop 3 x)

Номера инструментов можно посмотреть по справке к протоколу General Midi. Это дополнение к прото-

колу midi определяет какие номера каким инструментам должны соответствовать. Звучит ужасно, но звучит!

21.5 Пример

Опираясь на примитивы композиции, которые мы определил в модуле Score, мы можем написать мело-

дию. Ниже приведён небольшой пример. Инструменты:

closedHiHat = drum 42;

rideCymbal = drum 59;

cabasa = drum 69;

maracas

= drum 70;

tom

= drum 45;

flute

= instr 73;

piano

= instr 0;

Ударная секция:

b1 = bam 100

b0 = bam 84

drums1 = loop 80 $ chord [

tom

$ line [qn b1, qn b0, hnr],

maracas $ line [hnr, hn b0]

]

drums2 = quieter 20 $ cabasa $ loop 120 $ en $ line [b1, b0, b0, b0, b0]

drums3 = closedHiHat $ loop 50 $ en (line [b1, loop 12 wnr])

drums = drums1 =:= drums2 =:= drums3

Уже сейчас мы можем загрузить эту партию в интерпретатор и послушать, вызвав out drums. Аккорды к

мелодии:

c7

= chord [c, e, b]

gs7 = chord [low af, c, g]

g7

= chord [low g, low bf, f]

harmony = piano $ loop 12 $ lower 1 $ bn $ line [bn c7, gs7, g7]

Мелодия:

ac = louder 5

mel1 = bn $ line [bnr, subMel, ac $ stretch (1+1/8) e, c,

subMel, enr]

where subMel = line [g, stretch 1.5 $ qn g, qn f, qn g]

mel2 = loop 2 $ qn $ line [subMel, ac $ bn ds, c, d, ac $ bn c, c, c, wnr,

subMel, ac $ bn g, f, ds, ac $ bn f, ds, ac $ bn c]

where subMel = line [ac ds, c, d, ac $ bn c, c, c]

mel3 = loop 2 $ line [pat1 (high c) as g, pat1 g f d]

where pat1 a b c = line [pat a, loop 3 qnr, wnr,

pat b, qnr, hnr, pat c, qnr, hnr]

pat

x

= en (x +:+ x)

mel = flute $ line [mel1, mel2, mel3]

Пример | 315

Добавим в конце звук тарелки:

cha = delay (dur mel1 + dur mel2) $ loop 10 $ rideCymbal $ delay 1 b1

Соберём всё вместе и послушаем:

res = chord [

drums,

harmony,

high mel,

louder 40 cha,

rest 0]

main = out res

В конце стоит фиктивный элемент rest 0 для того чтобы было удобно глушить инструменты комменти-

рованием.

21.6 Эффективное представление музыкальной нотации

Реализация, которую мы рассмотрели не эффективна, Мы могли бы определить тип Track и по-другому.

Мы очень часто пользуемся операцией delay через операцию line. Так в выражении:

q = line [s1, s2, line [loop 2 s3, s4], s5]

Мы будем несколько раз обходить элемент s3 для каждого применения line. К примеру сначала мы

смести все элементы на 3, потом сместим на 5, потом на 10, но вместо этого мы могли бы сразу сместить

все элементы на 18 за один проход. Для этого мы можем закодировать преобразования событий во времени

в типе Track:

data Track t a = Track {

trackDur

:: t,

trackEvents :: TList t a

data TList t a = Empty | Single a | Append (TList t a) (TList t a)

| TFun (Tfm t) (TList t a)

data Tfm t = Tfm ! t ! t

Тип TList позволяет проводить быстрое объединение списков. Дополнительный конструктор TFun обо-

значает линейное преобразование списка во времени. Линейное преобразование кодируется двумя числами,

это масштаб и смещение. Мы считаем, что события в конструкторе Single начинаются в момент времени 0

и длятся 1 единицу времени. Так например событие, которое произошло на 2 единице времени и длилось 4

единицы можно представить так:

TFun (4 2) (Single a)

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