Создаём секвенсор в PureData

Автор: | 19.02.2012

Постановка задачи

Итак, наша цель: создать секвенсор на 16 шагов (под размер 4/4) с возможностью задавать темп, высоту ноты для каждого шага (как вручную так и с MIDI клавиатуры), и с MIDI-выходом. Ещё, наш секвенсор должен быть самостоятельным объектом, так, чтобы была возможность легко использовать его в других проектах.

Предварительно прикинем, что будет нужно для реализации наших замыслов. Как минимум, нам пригодятся:


* Массив из 16-ти элементов для хранения высоты нот;
* Какой-либо индикатор текущего шага;
* Кнопка Play/Stop;
* Какое-нибудь средство для задания темпа в BPM (ударах в минуту);
* Способ ввода нот с MIDI входа для записи и вывода их в MIDI-выход при проигрывании.

Поехали!

Ну что-ж, круг задач очерчен, вперёд! Запустим PureData и создадим новый проект. Добавим на пустое поле новый объект и введём в поле имени строку [pd sequencer]. В окне проекта появится прямоугольник нашего объекта:

Изображение

Также, откроется новое окно. Мы только что создали sub-patch (“подпатч”) – патч внутри патча. Создать sub-patch в своём проекте можно в любой момент, просто добавив объект с именем [pd], после которого, через пробел, можно написать имя нашего sub-patch (в данном случае мы ввели «sequencer»). По сути, создав sub-patch, мы создали новый объект, находящийся на тех же правах, что и стандартные объекты Pd, которые мы использовали до этого. Но, в данном случае, мы имеем возможность сами задавать его внутреннюю структуру.

Открывшееся пустое окно должно будет содержать внутренности нашего объекта. Пока что там пусто. Вспомним, какие входы и выходы должны быть у нашего секвенсора. Как минимум, это один MIDI вход для записи высоты ноты с внешнего источника, и два выхода – высота ноты и громкость (нужная в том числе и потому, что в стандарте MIDI окончание ноты задаётся той же нотой с громкостью 0).

Добавим нашему секвенсору входы и выходы. Для этого создадим объект [inlet] (он задаёт вход) и два объекта [outlet] (это выходы). Заметим, что имена этих объектов — без тильды на конце, так как эти входы/выходы предназначены для команд. Теперь внутри sub-patch у нас вот такое:

Изображение

А в основном окне видно, что наш объект приобрёл точки подсоединения – один вход и два выхода:

Изображение

Создание пользовательского интерфейса

Оставим на время основное окно в покое. Дальнейшие действия будем совершать во внутренностях нашего секвенсора. Если вы закроете окно sub-patch, войти в него всегда можно будет, кликнув на прямоугольнике объекта в родительском окне правой кнопкой мыши, и выбрав там Open. Или, как вариант, просто кликнув на объекте в режиме исполнения.

Тут стоит заметить, что отображение sub-patch в виде обычного блока-объекта — вовсе не единственный возможный вариант. В Pd также можно выводить объект в виде блока произвольного размера, содержащего элементы графического пользовательского интерфейса. Нам бы пригодилась эта функция, поскольку в дополнение к MIDI входам-выходам у нашего секвенсора должен быть и графический интерфейс для управления им, а также индикации режима работы.

Сделать такой интерфейс вовсе несложно. В окошке нашего sub-patch кликаем правой кнопкой на пустом месте и выбираем Properties. Откроется окошко «canvas»:

Изображение

Установим в нём галку в пункте «graph on parent». В окне sub-patch незамедлительно появится красный прямоугольник, а в родительском окне — серый прямоугольник на месте нашего объекта. Идея состоит в том, что все органы пользовательского графического интерфейса, которые в окне sub-patch будут помещены внутрь границ красного прямоугольника, будут отображены в соответствующем прямоугольнике родительского окна. Отображаться там они будут, только когда окно sub-patch закрыто. При его открытии в родительском окне опять будет отображён просто серый прямоугольник.

Размеры нашего интерфейса задаются в окне настройки в полях с именем «size». Также, в полях с именем «margin» можно задать смещение интерфейсного прямоугольника относительно начала координат.

В нашем случае, мы введём размеры 270 на 200. А ещё, мы хотим иметь в нашем интерфейсе симпатичный фон. Для этого добавим объект Canvas (“полотно”) (Put->Canvas). Подхватим его мышкой за верхний левый угол и установим его в верхний левый угол нашего интерфейсного прямоугольника. Кликнем правой кнопкой и войдём в его свойства. Откроется окошко с настройками:

Изображение

Здесь можно задать размеры и цвет полотна, а также параметры шрифта и текст, который будет выведен в указанном месте полотна. Настроим полотно так, чтобы оно заняло всю площадь интерфейсного прямоугольника, оставив лишь узкие полоски сверху и снизу (исключительно для того, чтобы не закрыть собой точки подключения входов и выходов). В качестве текста введём, например, “step sequencer”. У нас должно получиться что-то вроде этого:

Изображение

Теперь добавим важные объекты, которые будут служить одновременно и как функциональная часть нашего секвенсора, и как часть пользовательского интерфейса.

Сначала добавим Array (“массив”) (Put->Array). Откроется окошко с настройками:

Изображение

В этом массиве мы будем хранить значения высоты ноты для всех шестнадцати шагов секвенсора. Назовём наш массив «sequence», а в поле «size» изменим значение на 16. Также, включим опции в пунктах «save contents» (чтобы содержимое массива сохранялось вместе с сохранением проекта) и «draw as points» (чтобы содержимое массива отображалось не в виде непрерывного сигнала, а каждый элемент был отдельной точкой на графике).

Нажмём OK, и в окне появится визуальное отображение созданного массива. Кликнув на нём правой кнопкой мыши и войдя в свойства, мы увидим знакомое окно «canvas». На этот раз нас интересует не только размер полотна, но и содержимое полей «X range» и «Y range». Эти поля задают параметры графического отображения содержимого массива. Установим диапазон по оси Х от 0 до 16 (это будет номер ноты в секвенции), а по оси Y от 128 до 12 (это будет высота ноты в стандарте MIDI). Диапазон по Y специально указан в обратном порядке, чтобы более высокие ноты оказались в графике сверху. Поместим наш массив в интерфейсный прямоугольник и подгоним его размеры. В окошке свойств будет примерно такое:

Изображение

Также, мы можем сделать подложку для нашего массива любого цвета, просто создав и настроив объект Canvas и подложив его под Array. В данном случае был выбран белый цвет.

Теперь создадим ещё один важный элемент – селектор текущего шага. Исполним пункт меню Put->Hradio. В окошке появится горизонтальный многопозиционный переключатель. Установим его под нашим массивом и войдём в его свойства:

Изображение

Здесь мы настроим количество позиций переключения — 16. Также, введём в поля «send-symbol» и «receive-symbol» строку «step_selector». Это будет идентификатор для посылки сообщений селектору и чтения его текущего значения. Он пригодится нам позднее.

Теперь настроим размеры созданных объектов так, чтобы длина селектора совпадала с длиной массива нот, чтобы точно над каждой позицией селектора отображалась высота соответствующей ноты. К этому моменту в окошке sub-patch у нас должна была получиться примерно следующая картина:

Изображение

Основа интерфейса готова. Можно закрыть окно sub-patch и убедиться, что и в основном окне серый прямоугольник превратился в созданный нами интерфейс. Кликнем на нём правой кнопкой и выберем Open, чтобы открыть sub-patch и продолжить создание секвенсора. Основу интерфейса мы сделали. Пришло время приняться за логику.

Логика

Преобразование темпа

Темп в нашем секвенсоре будет задаваться в BPM. Но для организации его работы нам нужно будет знать значение задержки, соответствующей одному шагу. Это время легко сосчитать по формуле 60 / (tempo * 4), т.е. количество секунд в минуте поделить на темп и ещё на четыре, так как у нас 4 шага на бит. Поскольку в PureData время принято указывать в миллисекундах, то это значение дополнительно нужно будет умножить на 1000. В итоге имеем формулу 60000 / (tempo * 4).

Реализуем эту формулу в виде схемы на Рd. Созадим объект Number, который будет задавать темп, умножим его значение на 4, а затем поделим 60000 на то, что получилось. Поскольку объект [/] производит операцию деления в момент обновления данных на первом входе, а у нас там константа, нам придётся при изменении темпа посылать на первый вход делителя дополнительный сигнал. В качестве такого сигнала-триггера в Pd принято использовать сигнал «bang». Как получить такой сигнал в момент обновления темпа? Очень просто. Используем объект [bang], который генерирует сигнал-триггер на выходе при обновлении данных на входе. В конец цепочки повесим Number для контроля. Получилась вот такая схемка:

Изображение

Проверяем работу, меняя темп и наблюдая за тем, что получается на выходе. Работает. Чтобы не усложнять итоговую схему, спрячем реализацию получившегося конвертера темпа в sub-patch. Создадим объект [pd tempo], и в открывшееся окно переносим нашу схему (выделяем мышкой, Ctrl+X, Ctrl+V в окне sub-patch). Удаляем объекты Number, и вставляем вместо них [inlet] и [outlet]:

Изображение

В окошке секвенсора наш конвертер темпа теперь выглядит так:

Изображение

На вход он принимает темп в BPM, а на выходе возвращает время задержки, соответствующее одному шагу секвенсора.

Организация цикла

Следующим шагом нам необходимо создать схему, которая будет переключать текущую позицию секвенсора с заданной задержкой, и иметь возможность останавливаться и возобновлять работу по указанию пользователя. Сразу приведём схему:

Изображение

Принцип работы достаточно прост. Зелёная кнопка “play” – это объект типа Toggle. Он может быть в двух состояниях – “вкл” и “выкл”, и, соответственно, меняя состояние, выдаёт на выход 1 или 0. Переключается он просто кликом мышкой.

Объект [metro] – это метроном, выдающий на выход сигнал “bang” с указанным в миллисекундах периодом. Период можно так же задавать, подавая его на второй вход. Именно это свойство объекта [metro] и используется в схеме – на его второй вход подаётся значение задержки после преобразователя темпа.

Сигнал “bang” от метронома служит триггером для объекта [float], который по сигналу выдаёт на выход значение, предварительно записанное в него через его второй вход. Выход же объекта [float] увеличивается на единицу и снова записывается обратно. В результате получается, что значение, выдаваемое объектом [float], увеличивается на единицу каждый раз, когда пришло время переключить шаг в секвенсоре.

Но как сделать, чтобы при достижении конца секвенции счёт начинался сначала? Для этого текущее значение шага анализируется объектом [select]. Параметр, заданный в этом объекте (у нас это 15), сравнивается со значением на входе, и, в случае совпадения, объект генерирует сигнал “bang”. Этот сигнал, в свою очередь, вынуждает сообщение c числом -1 быть посланным на вход объекта [float], где оно тут же инкрементируется, и текущее значение становится равным 0. В итоге, наш шаг меняется циклически от 0 до 15.

В схеме также использован объект [s] (это допустимое сокращение от [send]) – на самом деле он просто осуществляет посылку данных в точку с указанным идентификатором. На схеме это объект [r] (сокращение от [receive]). Использование таких объектов позволяет упростить схему с многочисленными связями. Например, в нашем случае пришлось бы проводить связь по диагонали, пересекая несколько объектов, что явно не сделало бы схему легче воспринимаемой. Кроме того, с помощью объектов [s] и [r] можно удобно передавать данные из одной точки сразу в несколько приёмников (создав несколько объектов [r] c одинаковым идентификатором).

Проверим, что схема работает. Запустим её кликом по “play”, и удостоверимся, что число в контрольном Number меняется от 0 до 15 и опять сбрасывается в ноль. Покрутим темп, и проверим, что скорость соответствует изменению темпа.

Теперь оформим наш цикл в виде sub-patch. Создадим объект [pd loop], и перенесём в него нашу схему, заодно заменив управляющие объекты на входы и выходы. Также, отправим значение задержки по адресу «duration». Оно нам пригодится позже. Получим такую схему:

Изображение

А в окне секвенсора вот такой объект:

Изображение

Первый его вход – переключатель play/stop, второй вход – темп, и на выходе имеем текущую позицию проигрывания.

Механизм проигрывания

Вот мы и создали основные логические блоки, из которых будет построен наш секвенсор. Теперь дело за малым – соединить всё это воедино и заставить играть мелодии. В этом деле нам помогут объекты [t], [tabread] и [makenote].

Логика проигрывания проста:
* Получаем текущую позицию;
* Устанавливаем её в селекторе шагов;
* Читаем номер ноты из элемента массива, соответствующего текущей позиции;
* Формируем MIDI-совместимые сообщения NoteOn и NoteOff;

Всё это реализуется вот такой несложной схемой:

Изображение

Текущая позиция проигрывания попадает с выхода нашего объекта-цикла на вход объекта [t] (сокращение от [trigger]). Этот объект посылает значение со входа на свои выходы, начиная справа налево. Два f в качестве параметров говорят о том, что на оба выхода будут отсылаться значения типа float (т.е. числа).

Таким образом, при переключении позиции в секвенсоре первым делом новое значение будет отослано объекту с приёмным идентификатором «step_selector». Помните, мы задавали этот идентификатор в окне свойств нашего шагового селектора? В нашем случае именно он первым получит новое значение и переключит своё состояние.

Далее, номер позиции получит объект [tabread]. Этот объект выдаёт на выход значение из таблицы/массива, имя которой указано в качестве параметра. Мы указали имя нашего массива с нотами. Это значит, что при каждом изменении позиции секвенсора объект [tabread] прочитает элемент из массива с номером, указанным на входе, и выдаст значение этого элемента на выходе. Это значение, как мы помним – номер ноты в стандарте MIDI.

Далее, мы подадим номер MIDI ноты на первый вход объекта [makenote]. Этот объект никак не изменяет входящие данные, и пропустит нашу ноту сквозь себя без изменения. Но вот далее он сам, через время, заданное на своём третьем входе, сгенерирует эту же ноту, но с нулевой громкостью (что в стандарте MIDI означает Note Off, конец ноты).
Для этого на третий вход этого объекта мы подадим длительность, ту самую, что выдаёт нам преобразователь темпа. Ведь в нашем секвенсоре длительность ноты всегда соответствует одному шагу.

Механизм записи

Осталось совсем ничего – добавить возможность записи нот с MIDI входа. Добавим к нашему секвенсору вот такую схему:

Изображение

Тут всё совсем просто. Число, пришедшее на вход (номер MIDI ноты), просто записывается объектом [tabwrite] в элемент массива с индексом, прочитанным из селектора шагов. То есть, в текущую ячейку секвенсора.

Ну всё, почти готово. Переместим кнопку “play” и регулятор темпа внутрь красного интерфейсного прямоугольника. Вот так теперь выглядит наш секвенсор изнутри:

Изображение

Закрываем окошко sub-patch, и вот он, наш секвенсор, снаружи. Один вход, два выхода, кнопка “play”, регулятор тембра, селектор шага, и возможность мышкой менять высоту нот:

Изображение

Использование

Проверим секвенсор в деле. Подключим к его MIDI-входу объект [notein], выдающий команды с MIDI порта, установленного в настройках PureData. Если у вас есть MIDI-клавиатура, можно будет запустить секвенсор и записать ноты прямо с неё. Если нет – не беда, можно установить нужный шаг селектором, и вручную установить номер ноты в объекте Number на входе секвенсора. Ну и, наконец, можно просто менять высоту нот мышкой.

Изображение

На выход секвенсора мы подключили объект [noteout]. В качестве параметра он принимает номер MIDI-канала. Этот объект выдаёт MIDI-команды в порт, указанный как выход в настройках PureData. Да, наш секвенсор может играть на любом «железном» синтезаторе. А для проверки работы можно указать в качестве выхода стандартный MIDI-синтезатор Windows, и услышать свою мелодию. А можно и просто повесить на выход секвенсора объект [mtof], преобразующий номер ноты в соответствующую ей частоту, а к нему прицепить [osc~], или один из генераторов звука, созданных нами в предыдущей статье.

Оформление в виде универсального объекта (abstraction)

Ну и напоследок. Чтобы сделать совсем отлично, перенесём содержимое из нашего sub-patch окна секвенсора в основное окно и сохраним в файл под коротким понятным именем, например “stepseq.pd”. Теперь в любом проекте, создаваемом в Pd, вы можете просто добавить объект с именем [stepseq], и будет добавлен наш секвенсор. Такая реализация в терминологии Pd называется abstraction («абстракция»). Единственным условием является то, чтобы файл “stepseq.pd” или лежал в текущей директории (той же, где создаётся проект), или путь к нему был указан в настройках PureData.

Важное замечание: всем идентификаторам, использованным в патче (в нашем случае это идентификаторы в объектах [r] и [s], а также имя массива и send/receive symbol в свойствах Hradio), очень желательно добавить префикс «$0-«, например, «duration» станет «$0-duration». Такой метод именования позволит без проблем использовать в патче несколько одинаковых объектов. Фишка в том, что при загрузке патча Pd заменит $0 на уникальное число, разное у каждой копии объекта. Таким образом, работе патча не будет мешать такая неприятность, как конфликт имён.

P.S. И ещё, в Pd просто отличный хэлп, написанный на Pd. Кликаем на любом объекте, и в контекстном меню выбираем Help. Тот факт, что хэлп написан на Pd, позволяет тут же запустить и проверить в работе схему из примера, или скопировать её к себе в проект.

Исходник: http://pastebin.com/WythEaFB. Сохраняем в текстовый файл, делаем расширение *.pd

© Rio FX