Pattr

Работа с большими проектами в Max/MSP

Давненько я уже не писал оригинального материала на pattr. Всегда было либо лень писать статьи о том, как «просто и непринужденно сделать нечто в Max/MSP», либо просто лень, либо не было идей, времени и т.д. Однако, все это время в голове зрела тема для статьи. Хотелось написать нечто более интересное, основанное на собственном опыте, чего нет в туториалах к максу.

В конце марта на новой сцене Александринского театра состоялась премьера медиаспектакля «Нейроинтегрум», в работе над которым я принимал участие. В мои задачи входило программирование элементов интеракции компьютера и перформера, работа с многоканальным аудио и некоторые другие задачи, связанные с управлением компьютерами, генерирующих графику. Все это вылилось в довольно большой патч: 115 файлов .maxpat, 207 коммитов на BitBucket, несколько Java-классов, написанных для специфичных целей, — все в целом (вместе с аудиофайлами) занимает 571 Мб. Думаю, материала из полученного опыта хватит на несколько статей.

Кое-что о технической стороне «Нейроинтегрума» можно узнать из статьи на хабре, которую я писал недавно, а здесь же я расскажу о некоторых особенностях работы с большими проектами в Max/MSP. Детальный разбор примененных технологий оставлю на следующий раз. Тех, кто заинтересовался, прошу под кат.

Организация проекта

В начале работы над «Нейроинтегрумом» я принял решение создать отдельный package для всех файлов проекта. Кто еще не знает, пэкэджи — это убер-удобная фича для организации библиотек, добавленная в шестой версии. Пэкэдж определяет структуру директорий, в которых должны располагаться патчи, хелпы, экстерналы, Java-классы и т.д. Организовав один раз свою библиотечку в таких директориях, ею можно легко делиться с другими — никому больше не придется вручную копировать файлы в папки Cycling '74/, patchers/extras, etc... ну вы поняли.

Папка с проектом на текущий момент выглядит так:
    neurointegrum-maxmsp/
    ├── java-classes
    ├── jsui
    ├── media
    ├── misc
    └── patchers

Названия папок говорят сами за себя. Можно лишь отметить, что в директории misc у меня лежат файлы с патчами, созданными специально для того, чтобы изолированно тестировать работу отдельных синтезаторов. Хотя, по-хорошему, такие патчи должны быть хелпами — но ладно, в следующий раз сделаю иначе.

В папке patchers у меня лежат все патчи. Среди них пять — особенные, они служат для запуска всего и вся: +cuescripts.maxpat, +mappings.maxpat, +scenes.maxpat, +sound.maxpat, +sur.controller.maxpat. Из названий можно догадаться, что в +sound находятся синтезаторы, а в +mappings — мэппинги для управления синтами. В +sur.controller располагается логика для управления многоканальным звуком; все синтезаторы так или иначе направляют звук в него. +scenes отвечает за управление параметрами в софте, генерирующем графику, который запущен на других компьютерах. Про +cuescripts поясню позже. Все эти патчи, запущенные и аккуратно расставленные, можно увидеть на следующем скриншоте:

Собственно, «Нейроинтегрум» целиком и полностью управлялся из этих патчей.

В шестом максе также имеется возможность создания projects. Это довольно удобный способ организации проектов, однако, у меня были две неудачные попытки перевести старые патчи средней сложности в этот формат, поэтому при работе над спектаклем решил использовать package. В будущем обязательно разберусь с этим, если не будет лень :)

Чтобы не взрывать вам мозг большим количеством технической инфы о том, как я юзаю систему контроля версий с проектами Max/MSP, расскажу в конце статьи.

Управление патчем

Max отлично подходит для создания небольших скетчей, проверки своих идей, но когда патч разрастается, работа превращается в ужас: бесконечное количество субпатчей, объектов send/receive непонятного назначения, бессмысленные комменты, сделанные бессонной ночью и так далее. А всем этим нужно еще и управлять, переключать состояние, добавлять новые фичи, тюнить алгоритмы... Мозг в какой-то момент просто теряет способность удерживать все связи. Для того, чтобы немного разгрузить голову, можно внести некоторую структуру в свою работу с Max.

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

Но Max очень высокоуровневый и нисколько не объектно-ориентированный, что бы не говорили про него не-программисты; большинство паттернов к нему просто неприменимы, либо применимы, но инструменты весьма трудны в освоении. В качестве такого примера можно взять библиотеку odot от CNMAT, авторы которой попытались привнести элементы ООП и других парадигм в Max/MSP посредством хитрого использования OSC-сообщений (!).

Jamoma

Но не все так плохо. В Max коммьюнити не раз задумывались над тем, как повысить продуктивность при работе с большими патчами. Одним из инструментов, решающих эту проблему, является фреймворк Jamoma. При первом знакомстве с ним мне казалось, что это просто набор объектов и различных хелперов. Это не так. Jamoma — это, в первую очередь, набор инструментов и правил для создания патчей. Он представляет из себя реализацию паттерна проектирования Model-View-Controller. Я не буду детально рассказывать о нем (интересующихся просьба воспользоваться wiki), скажу лишь, что его суть заключается в разделении бизнес-логики (например, алгоритмов синтеза), UI и части управления софтом.

Благодаря такому подходу, в патче «Нейроинтегрума» я могу спокойно создать новый синтезатор в одном месте, создать для него мэппинги в другом и управлять всем из третьего, не опасаясь сломать что-то.

Описание работы Jamoma само по себе тянет на несколько статей, поэтому в данной я лишь кратко опишу, как работаю с этим фреймворком.

Модули

Jamoma предполагает, что патч организуется из модулей. Модуль представляет из себя сделанный особым образом bpatcher, в котором инкапсулированы логика и UI. В идеале, этот модуль не должен иметь никаких прямых связей с другими модулями или частями патчей, однако, на практике, в том числе в связи с некоторыми особенностями работы Max, это не всегда удается соблюсти.

На скриншоте представлены внутренности модуля, управляющего зрительским освещением и стробоскопом в «Нейроинтегруме» по MIDI:

Слева вверху внешний вид модуля, справа внутренности bpatcher с UI, а слева внизу логика модуля. Обратите внимание, что UI-объекты не соединяются напрямую с логикой. Все изменения параметров поступают в объекты jcom.hub и предпоследний аутлет jcom.in, который соединен с алгоритмом, где и производится вся работа.

Каждый элемент UI имеет собственное имя, указываемое в первом аргументе объектов jcom.parameter, jcom.message или jcom.return. Любое изменение параметра приводит к передаче OSC-сообщения в jcom.hub и jcom.in. Например, если включить на toggle, то придет сообщение /light 1, если нажать на button, то /strobe. В абстракции или субпатче с алгоритмом эти сообщения разделяются посредством jcom.oscroute.

Так происходит разделение логики и UI в Jamoma.

Каждый модуль в Jamoma имеет уникальное имя, по которому можно обратиться к данному модулю из любой части патча (в нашем примере это /test_midi). Включить зрительский свет можно, отправив сообщение /test_midi/light 1 в объект [jcom.send jcom.remote.module.to]. Это включит toggle и отправит необходимые сообщения в jcom.hub и jcom.in:

Для того, чтобы изменить параметр какого-либо модуля достаточно лишь вспомнить его название и название параметра. Лично для меня это легче, чем вспомнить имя объекта receive.

Такой подход, когда имеется доступ ко всем параметрам модуля, позволяет легко описать состояние последнего с помощью последовательности OSC-сообщений. Чуть ниже мы увидим пример того, как это используется.

Кроме удаленного управления параметрами модулей, Jamoma также позволяет следить за изменениями параметров посредством объекта [jcom.receive jcom.remote.module.from] — эту фичу удобно использовать для создания мэппингов между контроллерами и синтами. Мэппинг можно поместить в отдельный субпатч, напрямую не связанный с другими частями патча, что (по моим личным ощущениям) очень сильно помогает производить поиск необходимого мэппинга, когда требуется его доработка, а также позволяет не загружать голову тем, откуда приходят те или иные сообщения.

CUE скрипты

И так, описав возможности по управлению модулями, мы подошли к одной из самых замечательных фишек в Jamoma — CUE-скрипты. Скрипт — это текстовый файл, который логически разбит на части, каждая из которых содержит набор OSC-сообщений, отправляемых в модули. В ходе работы патча можно переключаться между этими частями, тем самым меняя состояние всего патча. В контексте перформансов, такой скрипт позволяет мыслить об управлении патчем, как о переключении сцен: в разных сценах активируются разные синтезаторы, мэппинги, производятся какие-то действия и т.д. Для переключения по скрипту я сделал отдельный патч +cuescripts, окно которого всегда находится поверх остальных, чтобы можно было легко определить, на каком этапе находится спектакль.

И последнее о Jamoma

Прелесть Jamoma лично для меня заключается в том, что при условии работы «по ее правилам», можно добиться удивительной гибкости при работе с большим проектом. Как показала практика, действительно легко вносить изменения и добавлять фичи в уже разросшийся патч — из-за малого количества прямых связей между модулями можно не думать о том, что может что-то сломаться после внесенных изменений.

В Jamoma есть множество других прелестей (чего стоит только «упаковывание» нескольких аудио сигналов в один патчкорд посредством jcom.pack≈). Тем, кто заинтересовался, рекомендую ознакомиться с материалами на официальном сайте.

Советы и прочее

О самом главном инструменте при работе с большими патчами (лично для меня) было написано выше. Ниже я расскажу о некоторых полезных привычках, которые выработал за все время работы с Max.

Research then use!

Из работы над «Нейроинтегрумом» я вынес очень важный урок: стоит всегда разделять работу над проектом и работу над исследованием какой-либо технологии. На практике это означает, что в продакшене надо стараться не использовать технологию, работу которой ты примерно понимаешь, но на практике не юзал. Это отнимает много времени, а результат может не оправдать ожидания.

Пример. В первой сцене спектакля рисуется генератив, похожий на трещины. Мне показалось, что для озвучивания этой сцены отлично подойдет икрамовский синт CataRT. Синт был разобран, упакован в модуль Jamoma (модуль /cracks в первом скриншоте), к нему было сделаны мэппинги, добавлена поддержка многоканального звука. Но в реальных условиях патч звучал ужасно.

В целом, работы по портированию этого синта в наш патч и по улучшению его звучания (иными словами, попытки допилить изначально плохую идею) заняли около недели. В итоге, в спектакле он все еще звучит, но только в очень маленькой части, в самом начале при повороте головы перформера. Сами же «трещины» озвучиваются более простым синтом, собранным за день вместе с Олегом Макаровым.

Этому разделу вполне можно было бы дать название в духе «Не усложняй», но, на мой вкус, это звучит как оправдание для того, чтобы ничем не интересоваться. Хотя в итоге CataRT почти не юзается в спектакле, при работе с ним я обнаружил один интересный прием по роутингу сообщений, который очень сильно облегчил мне жизнь (опишу его в отдельной статье как-нибудь); также я разобрался, как работает garbage collector в FTM, и пришло осознание по некоторым тонкостям Max/MSP в целом. Так что усилия были не напрасны в любом случае :)

Naming conventions

Названия всех файлов проекта я начинаю с какого-нибудь префикса. Например, в «Нейроинтегруме» был префикс “n”. Если имя файла составное, то части слова разделяю точками. Исключения составляют только «особые» патчи, имена которых начинаются с «+» — для того, чтобы их можно было легко найти, отсортировав список файлов по имени. Аналогичное правило применяю и при именовании send, receive, value, а также субпатчей.

Думаю, стоит также отметить, что в одном из основных патчей в комментарии я на видном месте перечисляю все глобальные объекты send, receive и value. Это можно увидеть на первом скриншоте в окне +sur.controller.

Coloring

Как бы забавно это не звучало, но одной из самых полезных привычек, выработанных у меня является привычка раскрашивать объекты и патчкорды. Не все подряд, конечно. Например, все send/receive у меня зеленые, а value синие. Логика проста: send/receive представляют из себя своего рода «дыры» в патче, а value некое состояние, которое используется в нескольких местах, — поэтому неплохо как-то выделять эти частные случаи.

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

Использование системы контроля версий с Max/MSP

В нашей команде в качестве VCS используется Mercurial. На вопрос о том, почему не выбрали Git, отвечу просто: так сложилось. Вернее, так сложилось, что при выборе инструментов со мной никто не стал спорить на тему “Hg vs. Git”, — выбор был чисто субъективным.

О том, как правильно работать с VCS написано много статей (рекомендую). В целом, все подобные советы применимы и к проектам Max/MSP, поэтому в этой части я просто опишу, как делаю сам.

При работе с репозиторием я использую традиционный подход. В основной ветке у меня лежит 100% работающий код, который можно всегда запустить и показать кому-нибудь. Для работы над отдельными сценами спектакля или специфичными вещами (например, каким-нибудь сложным синтом) создается отдельная ветка, которая по завершении работы сливается с основной.

Все довольно стандартно, но есть одно «но». Патчи Max/MSP — это .json файлы, в которых описываются связи между объектами. Можно определенно понять некоторые вещи, открыв их текстовым редактором, однако, как правило, какой-то ценной информации, из которой можно что-то сказать о структуре патча, вынести из этого не получится. Если в случае с текстовыми языками программирования команда hg diff дает адекватное представление об изменениях в коде, то с максом такой фокус не пройдет. Смотрите сами:

Вывод hg diff

diff -r 536fb31332c6 misc/4sins.maxpat
--- a/misc/4sins.maxpat Fri Mar 21 21:10:27 2014 +0400
+++ b/misc/4sins.maxpat Fri Mar 21 23:22:48 2014 +0400
@@ -102,7 +102,7 @@
"numoutlets" : 2,
"outlettype" : [ "", "" ],
"parameter_enable" : 0,
- "patching_rect" : [ 186.0, 19.0, 73.0, 397.0 ],
+ "patching_rect" : [ 56.0, 19.0, 73.0, 397.0 ],
"setminmax" : [ 40.0, 120.0 ],
"size" : 4
}
@@ -126,7 +126,7 @@
"architecture" : "x86"
}
,
- "rect" : [ 713.0, 517.0, 640.0, 480.0 ],
+ "rect" : [ 306.0, 101.0, 640.0, 480.0 ],
"bglocked" : 0,
"openinpresentation" : 0,
"default_fontsize" : 9.0,
@@ -147,6 +147,29 @@
"tags" : "",
"boxes" : [ {
"box" : {
+ "comment" : "",
+ "id" : "obj-9",
+ "maxclass" : "outlet",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 256.0, 339.0, 25.0, 25.0 ],
+ "presentation_rect" : [ 256.0, 335.0, 0.0, 0.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "comment" : "",
+ "id" : "obj-6",
+ "maxclass" : "outlet",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 141.0, 339.0, 25.0, 25.0 ]
+ }
+
+ }
+, {
+ "box" : {
"fontname" : "Arial",
"fontsize" : 9.0,
"id" : "obj-10",
@@ -241,8 +264,7 @@
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "signal" ],
- "patching_rect" : [ 229.0, 176.0, 32.5, 17.0 ],
- "presentation_rect" : [ 229.0, 174.0, 0.0, 0.0 ],
+ "patching_rect" : [ 294.0, 210.0, 32.5, 17.0 ],
"text" : "*~ 0."
}

@@ -256,8 +278,7 @@
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "signal" ],
- "patching_rect" : [ 188.25, 176.0, 32.5, 17.0 ],
- "presentation_rect" : [ 188.25, 174.0, 0.0, 0.0 ],
+ "patching_rect" : [ 180.25, 254.0, 32.5, 17.0 ],
"text" : "*~ 0."
}

@@ -272,7 +293,6 @@
"numoutlets" : 1,
"outlettype" : [ "signal" ],
"patching_rect" : [ 126.0, 176.0, 32.5, 17.0 ],
- "presentation_rect" : [ 126.25, 176.0, 0.0, 0.0 ],
"text" : "*~ 0."
}

@@ -399,17 +419,6 @@
, {
"box" : {
"comment" : "",
- "id" : "obj-56",
- "maxclass" : "outlet",
- "numinlets" : 1,
- "numoutlets" : 0,
- "patching_rect" : [ 126.0, 238.0, 25.0, 25.0 ]
- }
-
- }
-, {
- "box" : {
- "comment" : "",
"id" : "obj-70",
"maxclass" : "outlet",
"numinlets" : 1,
@@ -418,17 +427,6 @@
}

}
-, {
- "box" : {
- "comment" : "",
- "id" : "obj-87",
- "maxclass" : "outlet",
- "numinlets" : 1,
- "numoutlets" : 0,
- "patching_rect" : [ 224.0, 238.0, 25.0, 25.0 ]
- }
-
- }
],
"lines" : [ {
"patchline" : {
@@ -532,7 +530,7 @@
}
, {
"patchline" : {
- "destination" : [ "obj-56", 0 ],
+ "destination" : [ "obj-6", 0 ],
"disabled" : 0,
"hidden" : 0,
"source" : [ "obj-2", 0 ]
@@ -559,7 +557,7 @@
}
, {
"patchline" : {
- "destination" : [ "obj-87", 0 ],
+ "destination" : [ "obj-9", 0 ],
"disabled" : 0,
"hidden" : 0,
"source" : [ "obj-4", 0 ]
@@ -720,15 +718,6 @@
"destination" : [ "obj-2", 0 ],
"disabled" : 0,
"hidden" : 0,
- "source" : [ "obj-91", 1 ]
- }
-
- }
-, {
- "patchline" : {
- "destination" : [ "obj-2", 0 ],
- "disabled" : 0,
- "hidden" : 0,
"source" : [ "obj-91", 0 ]
}

diff -r 536fb31332c6 misc/test.midi.maxpat
--- a/misc/test.midi.maxpat Fri Mar 21 21:10:27 2014 +0400
+++ b/misc/test.midi.maxpat Fri Mar 21 23:22:48 2014 +0400
@@ -31,14 +31,26 @@
"box" : {
"fontname" : "Arial",
"fontsize" : 12.0,
+ "id" : "obj-2",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 87.0, 190.0, 61.0, 20.0 ],
+ "text" : "print AAA"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Arial",
+ "fontsize" : 12.0,
"id" : "obj-4",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
- "patching_rect" : [ 88.0, 74.0, 130.0, 18.0 ],
- "presentation_rect" : [ 348.0, 162.0, 0.0, 0.0 ],
- "text" : "/makenote 65 100 300"
+ "patching_rect" : [ 128.0, 64.0, 134.0, 18.0 ],
+ "text" : "/makenote 65 1111 300"
}

}
@@ -65,14 +77,23 @@
"numinlets" : 1,
"numoutlets" : 1,
"outlettype" : [ "" ],
- "patching_rect" : [ 37.0, 103.0, 150.0, 70.0 ],
- "presentation_rect" : [ 0.0, 0.0, 150.0, 70.0 ]
+ "patching_rect" : [ 37.0, 147.0, 150.0, 35.0 ],
+ "presentation_rect" : [ 0.0, 0.0, 150.0, 35.0 ]
}

}
],
"lines" : [ {
"patchline" : {
+ "destination" : [ "obj-2", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-1", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
"destination" : [ "obj-1", 0 ],
"disabled" : 0,
"hidden" : 0,
@@ -83,7 +104,7 @@
],
"dependency_cache" : [ {
"name" : "n.midi.maxpat",
- "bootpath" : "/Applications/Max 6.1/packages/ni-maxmsp/patchers",
+ "bootpath" : "/Applications/Max 6.1/packages/neurointegrum-maxmsp/patchers",
"patcherrelativepath" : "../patchers",
"type" : "JSON",
"implicit" : 1
@@ -123,6 +144,10 @@
"name" : "jcom.message.mxo",
"type" : "iLaX"
}
+, {
+ "name" : "jcom.return.mxo",
+ "type" : "iLaX"
+ }
]
}

Ужас, не правда ли? А ведь я внес совсем небольшие изменения в пару патчей: поменял расположение, добавил/удалил несколько объектов.

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

Набив некоторое количество шишек, изрядно нагадив в истории коммитов, был найден идеальный вариант решения этой проблемы: полностью отказаться от автоматического разрешения конфликтов. В моем случае процесс мержа обычно является слиянием изменений в default-ветку, поэтому логично предположить, что изменения главной ветке де-факто устаревшие, и их можно игнорировать при возникновении конфликта. Как раз для такого случая в Hg имеется встроенный mergetool под названием internal:other, который в конфликтной ситуации просто выбирает версию не из рабочей копии.

Лучше покажу пример. Вот так, находясь в ветке default, я сливаю в нее изменения из ветки scene_2: hg merge --tool=internal:other scene_2.

С мержами разобрались. Двигаемся дальше.

У меня есть определенная система комментирования коммитов:
[fix] n.breathing now has central speaker fade parameter [+] CUE scene for fading breathing with noise

В каждом комменте я стараюсь перечислить сделанные изменения. Перед каждым изменением в квадратных скобках ставлю тэг с очевидным смыслом ([+], [-], [*], [fix], [merge]), после чего небольшой текст. Это именно то, что позволяет в дальнейшем копаться в истории, и оперативно откатывать изменения предыдущих коммитов.

Очень забавно прослеживать, как меняется текст коммитов при приближении дедлайна. Для примера, сравните следующие комментарии с предыдущим:
[all done?]
[lol]
[...]
и так далее :)

Редко какой проект Max/MSP обходится без медиа-файлов. Однако, Mercurial (да и Git тоже) не любит массивные бинарные файлы — в больших по объему проектах с множеством таких файлов операции, типа pull, push и clone, могут выполняться по несколько минут, а то и вовсе не закончиться, сославшись на недостаток оперативной памяти на сервере, где крутится VCS. Иными словами, тречить бинарные файлы в Hg — плохая идея. Существуют способы обхода этой ситуации, однако, BitBucket с ними не работает, а поднимать и обслуживать свой сервер — лишний геморрой, которым не особо хочется заниматься.

Мой выход из этой ситуации прост — все медиа-файлы хранятся в директории media пэкэджа с проектом, содержимое которой добавлено .hgignore. Сама же папочка время от времени синхронизируется с Google Drive.

Подытожу вышесказанное. Использовать VCS для Max/MSP проектов можно и нужно. Описанные выше проблемы решаются довольно просто, а ценность возможности откатиться в любой момент на предыдущую версию, посмотреть, как было сделано раньше, сложно недооценить.

Zzz

Статья вышла несколько сумбурной: в ней имеются и практические советы, и байки, и техническая инфа — но иначе быть и не могло, потому что опыт субъективен. В статье я описал, как делаю сам; надеюсь, кому-нибудь это будет полезно :)

15 Apr 2014  OSCII