Ханс Петров
Введение в классы Фреймворка

Предпосылки

Мое путешествие в мир сценариев MIDI Remote от Ableton началось с поиска лучшего способа настройки моего FCB1010 в качестве контроллера для Live. Прошло не так много времени, прежде чем я понял: чтобы полностью настроить установку под свои потребности, я должен исследовать возможности подражания и узнать что-нибудь о написании сценариев на Питоне. Результаты моих исследований задокументированы здесь в надежде, что они смогут принести кому-нибудь пользу.

Если вы сюда пришли, то вы, вероятно, уже кое-что знаете об управляющих поверхностях (control surfaces), и вы наверняка знаете, что Live имеет встроенную поддержку для многих контроллеров. (Но если вы не знакомы с основными процедурами настройки контроллера, посмотрите раздел MIDI and Key Remote Control в руководстве Live, или ознакомьтесь с «Control Surface Lessons» в окне помощи программы)

Live предоставляет контроллерам возможность «мгновенного назначения», основанного на использовании сценариев MIDI Remote. Сценарии MIDI Remote написаны на языке программирования Python и, по сути, занимаются переводом MIDI-сообщений в инструкции, управляющие различными аспектами приложения Live. Каждый из контроллеров, поддерживаемых в Live, имеет отдельную папку с предназначенными для него сценариями. Позже мы обсудим это подробнее — но сперва немного истории.

Подражание

Всё началось довольно давно, когда вечно любопытные и изобретательные пользователи Live обнаружили, что можно воспользоваться возможностями «мгновенного назначения» для подражания какому-либо поддерживаемому контроллеру (как правило, таким способом «прикручивался» неподдерживаемый контроллер). Наиболее известная модель для подражания — Mackie.

Суть подражания: как бы обмануть Live — якобы вы подключили определенную часть MIDI-аппаратуры, когда на самом деле это не так. Трудность заключается в том, что ваше оборудование должно быть в состоянии обеспечивать Live такими MIDI-сообщениями, которые он ожидает от подражаемого контроллера. Подобное может быть осуществлено либо путём изменения конфигурации контроллера, либо через фильтрацию промежуточным приложением (таким как MIDI-OX). Такое подражание, по сути, является «методом чёрного ящика», так как сценарии MIDI Remote не изменяются (и нет необходимости понимать их внутреннее устройство, чтобы всё работало). Подражание в манере чёрного ящика несколько ограничивает — и следующим шагом было исследование самих сценариев.

Файлы сценариев

Сценарии устанавливаются вместе с приложением Live. Если ваша операционная система — Windows, вы найдёте их здесь (или в подобном месте):

C:\Program Files\Ableton\Live 8.x.x\Resources\MIDI Remote Scripts\

Типичный каталог со сценариями MIDI Remote содержит ряд папок с именами, похожими на следующие:

_Axiom
_Framework
_Generic
_MxDCore
_Tools
_UserScript
APC40
Axiom
AxiomPro
Axiom_25_Classic
Axiom_49_61_Classic
и т.д.

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

В каждой папке, как правило, расположен файл __init __.pyc, pyc-файл с именем контроллера и один или более дополнительных pyc-файлов. Например, для Vestax VCM600 в каталоге «VCM600» находятся следующие файлы:

__init__.pyc
VCM600.pyc
ViewTogglerComponent.pyc

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

Исходники

Pyc-файлы Питона сравнительно легко декомпилировать, а полученные py-файлы вполне читабельны (на самом деле они практически идентичны оригинальным исходным файлам).

Файлы Питона можно декомпилировать различными способами. Проект «Decompyle» на Sourceforge (среди прочих) хорошо работает с файлами Питона до версии 2.3. Существуют также онлайн-сервисы «депитонирования», поддерживающие файлы более поздних версий Python, но увы, нет бесплатных сервисов, способных обрабатывать файлы версии 2.5.

Версию pyc-файла можно определить, изучив первые четыре байта в шестнадцатеричном редакторе. «Волшебные числа» следующие:

99 4e 0d 0a — python 1.5
Fc c4 0d 0a — python 1.6
87 c6 0d 0a — python 2.0
2a eb 0d 0a — python 2.1
2d ed 0d 0a — python 2.2
3b f2 0d 0a — python 2.3
6d f2 0d 0a — python 2.4
B3 f2 0d 0a — python 2.5

Сценарии Live 7.x.x, как правило, были написаны на Питоне версии 2.2, в то время как сценарии Live 8.x.x, как правило — на Питоне версии 2.5 (к сожалению). Сценарии Live 7.0.13 в декомпилированном формате .py были некоторое время доступны здесь. Эти файлы оказались чрезвычайно полезными в качестве справочника для понимания сценариев.

Ранние исследования декомпилированных сценариев были сосредоточены на часто используемом файле consts.py. Этот файл используется во многих ранних сценариях для определения констант — в том числе задающих отображения MIDI-нот для управляющих поверхностей. Нет больше необходимости рыться в таблицах реализации MIDI, или вручную определять назначения MIDI-нот — всё это есть в файлах. Изменение файла consts.py было простым способом осуществить подражание, и многие взялись за создание новых пользовательских сценариев с нуля — иногда очень сложных.

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

_Framework-сценарии

Ранее в «сценарном» деле, кажется, каждый был сам за себя. OEM-производителям, которым хотелось осуществить родную поддержку «мгновенного назначения», по-видимому, приходилось писать свои собственные python-сценарии с множеством избыточного и плохорасширяемого кода. Для простых сценариев это не было большой проблемой, однако продвинутые сценарии часто состоят из множества файлов и сотен строк кода. Так как рынок управляющих поверхностей растёт и взрослеет, потребность в объединённом наборе вспомогательных сценариев становится очевидной. Похоже, что решение этого вопроса приходит — в форме сценариев Фреймворка.

Новые контроллеры уже широко используют классы Фреймворка (иногда исключительно только их). В их список входят Akai APC40, Novation Launchpad, M-Audio Axiom Pro, Vestax VCM600 и продукты от Open Labs. И этот список обязательно будет пополняться. Сценарии Фреймворка — это, по существу, набор вспомогательных классов — модульная библиотека классов и объектов, которая берёт на себя бо́льшую часть тяжёлой работы, а также снижает необходимость прямых вызовов LiveAPI.

Классы Фреймворка представляют собой «вторую половину» объектной модели Live (LOM — Live Object Model), которая проиллюстрирована в справочных документах Max for Live. Документы Max for Live в некоторых деталях описывают первую половину — Live API, и косвенно ссылаются на классы Фреймворка (control_surfaces).

Схема ранней версии объектной модели Live

Имена Компонентов и Контролов (элементов), упомянутые в документах Max for Live, точно соответствуют именам модулей Фреймворка. Сравните с именами сценарных файлов в каталоге _Framework, который находится в папке сценариев MIDI Remote (здесь имена отсортированы по типу):

Центральный базовый класс:
ControlSurface

Компоненты управляющей поверхности:
ControlSurfaceComponent
TransportComponent
SessionComponent
ClipSlotComponent
ChannelStripComponent
MixerComponent
DeviceComponent
CompoundComponent
ModeSelectorComponent
SceneComponent
SessionZoomingComponent
TrackEQComponent
TrackFilterComponent
ChannelTranslationSelector

Управляющие элементы:
ControlElement
ButtonElement
ButtonMatrixElement
ButtonSliderElement
EncoderElement
InputControlElement
NotifyingControlElement
PhysicalDisplayElement
SliderElement

Прочие классы:
DisplayDataSource
LogicalDisplaySegment

А теперь настало время изучить внутреннюю работу сценариев Фреймворка. Начнём мы с создания подходящих условий для редактирования.

Редактирование сценариев

Я обнаружил, что для любой нетривиальной работы со сценариями в большинстве случаев лучше использовать специальный редактор исходного кода (хотя бы потому, что необходимо контролировать пробелы, используемые в Питоне для отступов). Но на крайний случай для открытия и редактирования py-файла можно использовать практически любой текстовый редактор (только будьте осторожны — не смешайте табуляции с пробелами при редактировании). Лучше всего подойдёт интегрированная среда разработки (IDE). Например, я большинство работ по написанию сценариев сделал в Wing IDE. Бесплатная версия этой среды доступна здесь. Santi’s Python Editor (SPE) — еще одна Python IDE, на которую стоит обратить внимание. Да и вообще, есть множество альтернатив для разных операционных систем.

Установка самого Питона является неотъемлемой частью настройки интегрированной среды разработки. Питон может быть установлен как часть некоторых IDE, или же может быть установлен отдельно. В некоторых операционных системах (но не в Windows) Питон идёт в комплекте и уже установлен.

Строго говоря, для базовой работы со сценариями MIDI Remote не обязательно устанавливать Питон, так как компилятор Питона встроен в Live (если Live найдёт py-файл в каталоге сценариев MIDI Remote, то он попытается скомпилировать его при запуске). Тем не менее преимущества использования IDE проявятся только с установленным Питоном (в том числе радость автозавершения кода).

Типичный код сценария на Питоне выглядит следующим образом (в среде Wing IDE):

Снимок экрана с Wing IDE

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

Отладка

Live предоставляет несколько встроенных механизмов, которые могут упростить отладку. Я обнаружил, что первый ключ к отладке сценариев — задействовать файл журнала. Файл журнала представляет собой простой текстовый файл, который можно найти в каталоге «Preferences»:

C:\Documents and Settings\username\Application Data\Ableton\Live 8.x.x\Preferences\Log.txt

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

Вот пример плохого кода:

transport.set_foo_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 89))

И вот что отобразится в файле журнала:

4812 ms. RemoteScriptError: Traceback (most recent call last):
4813 ms. RemoteScriptError:   File "C:\Program Files\Ableton\Live 8.1\Resources\MIDI Remote Scripts\ProjectX\__init__.py", line 7, in create_instance
4813 ms. RemoteScriptError:     
4814 ms. RemoteScriptError: return ProjectX(c_instance)
4814 ms. RemoteScriptError:   File "C:\Program Files\Ableton\Live 8.1\Resources\MIDI Remote Scripts\ProjectX\ProjectX.py", line 48, in __init__
4815 ms. RemoteScriptError:     
4816 ms. RemoteScriptError: self._setup_transport_control() # Run the transport setup part of the script
4817 ms. RemoteScriptError:   File "C:\Program Files\Ableton\Live 8.1\Resources\MIDI Remote Scripts\ProjectX\ProjectX.py", line 78, in _setup_transport_control
4818 ms. RemoteScriptError:     
4819 ms. RemoteScriptError: transport.set_foo_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 89))
4819 ms. RemoteScriptError: AttributeError
4820 ms. RemoteScriptError: : 
4820 ms. RemoteScriptError: 'TransportComponent' object has no attribute 'set_foo_button'
4821 ms. RemoteScriptError: 

Файл журнала можно использовать и для слежки (используя метод Фреймворка log_message). Проследить можно практически за всем. Например, этот код

self.log_message("Captain's log stardate " + str(Live.Application.get_random_int(0, 20000)))

отобразит в файле журнала строку

255483 ms. RemoteScriptMessage: Captain's log stardate 5399

Когда я вношу изменения в python-сценарий, мне часто приходится перекомпилировать его и проверять его работоспособность (т.е. убеждаться, не испортил ли я чего). Я делаю это так: устанавливаю контроллер в выпадающем списке MIDI-настроек в положение «None», и сразу же выставляю имя своего сценария обратно. Такая процедура заставляет Live перекомпилировать изменённый сценарий и избавляет от необходимости каждый раз перезагружать приложение. (На самом деле такая процедура не гарантирует перекомпиляцию (по крайней мере в девятой версии Live). Понятно, что перезапускать программу для гарантированной перекомпиляции не очень удобно. Поэтому можно воспользоваться другим способом — просто создать новый Live-сет. В таком случае перекомпиляция выполнится гарантировано, и перезапуск не потребуется — прим. перев.)

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

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

Пример сценария

Нам нужно создать новую папку в каталоге сценариев MIDI Remote, которую мы в праве назвать так, как нам захочется (но нужно иметь в виду, что если имя начинается с символа подчеркивания, оно не будет отображаться в выпадающем списке в настройках). Давайте назовём папку «ААА», чтобы это название появилось на самом верху выпадающего списка.

Снимок настроек MIDI/Sync — список управляющих поверхностей

После этого нужно создать два файла и положить их в эту папку. Первый файл будет называться __init__.py. Этот файл помечает наш каталог как пакет Питона (для компилятора), и содержит всего несколько строк:

# __init__.py
from Transport import Transport
def create_instance(c_instance):
    return Transport(c_instance)

Далее создадим файл Transport.py, который будет основным файлом нашего сценария (обратите внимание, что имя файла не будет отображаться в выпадающем списке — важно только имя папки). Этот файл будет побольше (здесь и далее важные дополнения от переводчика выделены бледно-жёлтым):

# Transport.py 
# Это урезанный сценарий, который использует классы Фреймворка,
# чтобы назначить MIDI-ноты на воспроизведение, остановку и запись.

from __future__ import with_statement # Необходимо в Live 9
from _Framework.ControlSurface import ControlSurface
# Центральный базовый класс для сценариев, основанных на новом Фреймворке
from _Framework.TransportComponent import TransportComponent
# Класс, инкапсулирующий все функции транспортной секции Live
from _Framework.ButtonElement import ButtonElement
# Класс, представляющий кнопку на контроллере

class Transport(ControlSurface):
    def __init__(self, c_instance):
        ControlSurface.__init__(self, c_instance)
        with self.component_guard(): # Необходимо окружать код начиная с Live 9
            transport = TransportComponent() # Создание экземпляра транспортной компоненты
            # ButtonElement(<is_momentary>, <тип сообщения>, <канал>, <идентификатор>)
            transport.set_play_button(ButtonElement(True, 0, 0, 61)) # (тип сообщения «0» — это MIDI-нота)
            transport.set_stop_button(ButtonElement(True, 0, 0, 63))
            transport.set_record_button(ButtonElement(True, 0, 0, 66))

Теперь, если мы откроем Live и выберем «ААА» в выпадающем списке MIDI-настроек, Live скомпилирует наши py-файлы, создаст в каталоге сценария соответствующие pyc-файлы и запустит сценарий.

MIDI-ноты 60, 61 и 63 первого канала будут автоматически назначены на «воспроизведение», «стоп» и «запись» соответственно. Исходные коды этого простого сценария на Питоне можно найти здесь. Следуя такой же базовой структуре, мы вполне можем сделать много других простых отображений. Например, если мы хотим сопоставить какой-либо клавише функцию «tap tempo», то нам достаточно будет добавить в сценарий такую строку:

transport.set_tap_tempo_button(ButtonElement(True, 0, 0, 68))

Вышеприведённый сценарий использует один из самых основных модулей Фреймворка — модуль TransportComponent. Кроме трёх методов, описанных выше, у класса TransportComponent доступны ещё такие методы:

set_stop_button(button)
set_play_button(button)
set_seek_buttons(ffwd_button, rwd_button)
set_nudge_buttons(up_button, down_button)
set_record_button(button)
set_tap_tempo_button(button)
set_loop_button(button)
set_punch_buttons(in_button, out_button)
set_metronom_button(button)
set_overdub_button(sbutton)
set_tempo_control(control, fine_control)
set_song_position_control(control)

(Обратите внимание, что имя set_metronom_button в Фреймворке 7.x.x было написано с ошибкой, но в 8.x.x оно было исправлено на set_metronome_button. Отсюда следует, что сценарии, использующие этот метод, будут работать либо на седьмой версии программы, либо на восьмой, на какой именно — зависит от написания!)

Большинство файлов и функций Фреймворка (на самом деле классов и методов) говорят сами за себя, и достаточно взглянуть на имя класса или метода, чтобы понять его назначение. Другие же являются более сложными, и их удастся понять, обратившись к примерам кода. VCM600, Launchpad и Axiom Pro всё делают с использованием классов Фреймворка, как и APC40 (естественно). Эти большие сценарии можно использовать в качестве справочника (тайная ссылка для внимательных читателей здесь). Также можно ознакомиться с онлайн-документацией классов Фреймворка, сгенерированной из декомпилированных исходников. Опять же, все функции, по большей части, выполняют вполне ожидаемые действия.

При работе с декомпилированными исходниками стоит отметить, что многие сценарии написаны в духе «старой школы», ибо создавались они ещё до того, как были разработаны классы Фреймворка. Эти сценарии, конечно, работают, но, как правило, они гораздо сложнее новых. В настоящее время всю сложность обрабатывают классы Фреймворка, которые заметно упрощают большинство сценарных задач.

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

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

ProjectX

Сейчас мы точно не будем подражать таким контроллерам, как APC40 или Launchpad, потому что их возможности очень сильно привязаны к аппаратной части — хотя, по общему признанию, исследовать подражание APC40 было бы весьма интересно (но вряд ли это будет кому-то полезно, разве что, обладателям APC40, желающим настроить его под себя). Вместо этого мы с помощью классов Фреймворка превратим обыкновенную MIDI-клавиатуру в контроллер с двумерной сеткой.

MIDI-клавиатура, как правило, имеет одно измерение, а нам нужно реализовать некоторые возможности, доступные контроллерам с двумерной сеткой. Чтобы обойти это ограничение, мы будем использовать два набора клавиш — один будет соответствовать вертикали (сценам), а другой — горизонтали (дорожкам). Получится подвижная XY-сетка клавиш. Назовём наш «контроллер» ProjectX.

Сценарий «ProjectX» будет состоять из двух наборов клавиатурных назначений, которые могут быть использованы вместе или по отдельности. Часть «X» будет представлять собой вертикальный компонент сессии («красная рамка»), а часть «Y» — горизонтальный компонент сессии («желтая рамка»). Постараемся не усложнять сценарий (чтобы он, в конце концов, мог использоваться со стандартными MIDI-клавиатурами), но при этом продемонстрируем использование нескольких классов и методов Фреймворка — в первую очередь компонентов Сессии, Микшера и Транспорта.

Вот раскладка клавиатуры, показывающая, как мы будем назначать ноты.

Наглядное изображение раскладки клавиатуры

APC40, Launchpad и Monome имеют сетки из кнопок; здесь же мы разделили два измерения сетки на две сессионные рамки. «Красная рамка» будет охватывать семь сцен какой-либо дорожки, а «жёлтая рамка» охватит семь дорожек полосой в одну сцену. Красная рамка представляет собой набор из семи сцен (или клип-слотов), а жёлтая рамка представляет собой набор из семи дорожек. Вместе они образуют виртуальную сетку «семь дорожек на семь сцен», и будут управляться отдельными наборами из семи «белых клавиш». Вот как рамки выглядят в окне сессии (извините, что не видео):

Снимок экрана — как будут отображены рамки в окне сессии

А вот сценарий ProjectX (красная рамка), использующий магию Фреймворка:

from __future__ import with_statement # Необходимо в Live 9
import Live # Это позволяет нам (и методам Фреймворка) использовать Live API, если вдруг понадобится
import time # Мы будем использовать функции времени для фиксации времени при выводе в файл журнала

""" Ниже указаны все файлы Фреймворка, но в этом сценарии мы будем
    использовать только некоторые из них (остальные закомментированы) """

from _Framework.ButtonElement import ButtonElement
# Класс, представляющий кнопку на контроллере

#from _Framework.ButtonMatrixElement import ButtonMatrixElement
# Класс, представляющий двумерный набор кнопок

#from _Framework.ButtonSliderElement import ButtonSliderElement
# Класс, представляющий набор кнопок, используемых в качестве слайдера

from _Framework.ChannelStripComponent import ChannelStripComponent
# Класс, подсоединяющий дорожку к микшеру

#from _Framework.ChannelTranslationSelector import ChannelTranslationSelector
# Класс, переключающий режимы путём преобразования канала для сообщений заданного контрола

from _Framework.ClipSlotComponent import ClipSlotComponent
# Класс, представляющий слот клипа в Live

from _Framework.CompoundComponent import CompoundComponent
# Базовый класс для классов, объединяющих другие компоненты для формирования сложных компонентов

from _Framework.ControlElement import ControlElement
# Базовый класс для всех классов, представляющих элементы управления на контроллере

from _Framework.ControlSurface import ControlSurface
# Центральный базовый класс для сценариев, основанных на новом Фреймворке

from _Framework.ControlSurfaceComponent import ControlSurfaceComponent
# Базовый класс для всех классов, инкапсулирующих функции Live

#from _Framework.DeviceComponent import DeviceComponent
# Класс, представляющий устройства Live

#from _Framework.DisplayDataSource import DisplayDataSource
# Объект с данными, который подаётся с заданной строкой и уведомляет своих наблюдателей

#from _Framework.EncoderElement import EncoderElement
# Класс, представляющий элемент управления с непрерывным изменением значения

from _Framework.InputControlElement import *
# Базовый класс для всех классов, представляющих элементы управления на контроллере

#from _Framework.LogicalDisplaySegment import LogicalDisplaySegment
# Класс, представляющий определённый сегмент дисплея на контроллере

from _Framework.MixerComponent import MixerComponent
# Класс охватывает несколько каналов, тем самым формирует микшер

#from _Framework.ModeSelectorComponent import ModeSelectorComponent
# Класс для переключения между режимами, чтобы можно было назначать
# различные функции одним и тем же элементам управления

#from _Framework.NotifyingControlElement import NotifyingControlElement
# Класс, представляющий элементы управления, умеющие посылать значения

#from _Framework.PhysicalDisplayElement import PhysicalDisplayElement
# Класс, представляющий дисплей на контроллере

from _Framework.SceneComponent import SceneComponent
# Класс, представляющий сцену Live

from _Framework.SessionComponent import SessionComponent
# Класс охватывает несколько сцен, чтобы покрыть определённую часть сессии Live

from _Framework.SessionZoomingComponent import SessionZoomingComponent
# Класс, использующий матрицу из кнопок для выбора блока клипов сессии 

from _Framework.SliderElement import SliderElement
# Класс, представляющий слайдер на контроллере

#from _Framework.TrackEQComponent import TrackEQComponent
# Класс, представляющий эквалайзер дорожки. Он подцепляется к последнему эквалайзеру на дорожке

#from _Framework.TrackFilterComponent import TrackFilterComponent
# Класс, представляющий фильтр дорожки. Подцепляется к последнему фильтру на дорожке

from _Framework.TransportComponent import TransportComponent
# Класс, инкапсулирующий все функции транспортной секции Live

""" Здесь мы определяем несколько глобальных переменных """
CHANNEL = 0 # Каналы нумеруются от нуля до пятнадцати
# Этот сценарий использует лишь один MIDI-канал (Channel 1)
session = None # Глобальный объект сессии. Поскольку он глобальный, мы можем управлять им отовсюду
mixer = None # Глобальный объект микшера. Поскольку он глобальный, мы можем управлять им отовсюду

class ProjectX(ControlSurface):
    __module__ = __name__
    __doc__ = " ProjectX keyboard controller script "
    
    def __init__(self, c_instance):
        """Отсюда запускается всё, кроме переопределённых '_on_selected_track_changed' и 'disconnect' """
        ControlSurface.__init__(self, c_instance)
        with self.component_guard(): # Необходимо окружать код начиная с Live 9
            self.log_message(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime()) + \
                                            "--------------= ProjectX log opened =--------------")
            # Пишет сообщение в файл журнала. Является методом класса ControlSurface.
            self._set_suppress_rebuild_requests(True) # ранее self.set_suppress_rebuild_requests(True)
            # Отключает перестройку MIDI-соответствия до тех пор, пока мы не настроимся
            # В Live 9 имя этого метода начинается с подчёркивания — прим. перев.
            """Теперь вызываем наши собственные методы, описание которых будет ниже"""
            self._setup_transport_control() # Запускает часть сценария, отвечающую за установку транспорта
            self._setup_mixer_control() # Устанавливает объект микшера
            self._setup_session_control() # Устанавливает объект сессии

            """ Ниже кое-что из Live API, просто ради забавы """
            app = Live.Application.get_application() # получает дескриптор приложения
            maj = app.get_major_version() # получает основной номер версии приложения
            min = app.get_minor_version() # получает дополнительный номер версии приложения
            bug = app.get_bugfix_version() # получает номер исправления приложения
            self.show_message(str(maj) + "." + str(min) + "." + str(bug))
            # соединяет их и показывает информацию о версии в статусной строке,
            # используя метод show_message класса ControlSurface

            self._set_suppress_rebuild_requests(False)  # self.set_suppress_rebuild_requests(False)
            # Теперь, когда мы закончили настраиваться, включаем перестройку обратно
            # В Live 9 имя этого метода начинается с подчёркивания — прим. перев.

    def _setup_transport_control(self):
        is_momentary = True # Мы будем использовать только кнопки, активные в момент нажатия
        transport = TransportComponent() # Создание экземпляра транспортной компоненты
        """установка кнопок"""
        def BE(note): # Вспомогательная функция, позволяющая задавать кнопки более кратко
            return ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, note)
            # ButtonElement(<is_momentary>, <тип сообщения>, <канал>, <идентификатор>)
            # Следует отметить, что константа MIDI_NOTE_TYPE определена в модуле InputControlElement
        transport.set_play_button(BE(61))
        transport.set_stop_button(BE(63))
        transport.set_record_button(BE(66))
        transport.set_overdub_button(BE(68))
        nudge_up_button = BE(75); nudge_down_button = BE(73)
        transport.set_nudge_buttons(nudge_up_button, nudge_down_button)
        transport.set_tap_tempo_button(BE(78))
        transport.set_metronome_button(BE(80)) # По какой-то причине в версии 7.x.x
        # в имени этого метода не было буквы «е», и он назывался set_metronom_button()
        transport.set_loop_button(BE(82))
        punch_in_button = BE(85); punch_out_button = BE(87)
        transport.set_punch_buttons(punch_in_button, punch_out_button)
        ffwd_button = BE(90); rwd_button = BE(92)
        transport.set_seek_buttons(ffwd_button, rwd_button)
        """установка слайдеров"""
        tempo_control = SliderElement(MIDI_CC_TYPE, CHANNEL, 26)
        fine_tempo_control = SliderElement(MIDI_CC_TYPE, CHANNEL, 25)
        song_position_control = SliderElement(MIDI_CC_TYPE, CHANNEL, 24)
        transport.set_tempo_control(tempo_control, fine_tempo_control)
        transport.set_song_position_control(song_position_control)

    def _setup_mixer_control(self):
        is_momentary = True
        num_tracks = 7 # Микшер одномерный; здесь мы определяем его ширину в дорожках — семь столбцов,
        # которые мы назначим на семь «белых» нот
        """Здесь мы устанавливаем глобальный микшер"""
        # Следует отметить, что можно сделать сколько угодно микшеров
        global mixer # Мы хотим создать экземпляр глобального микшера как объект класса MixerComponent
        # (до сих пор он имел глобальный тип «None»)
        mixer = MixerComponent(num_tracks, 2, with_eqs=True, with_filters=True)
        # (<число дорожек>, <число возвратов>, <с эквалайзерами>, <с фильтрами>)
        mixer.set_track_offset(0) # Устанавливает начальную точку для полосы микшера (смещение слева)
        self.song().view.selected_track = mixer.channel_strip(0)._track
        # Устанавливает выбор полосы на первую дорожку, так что мы, к примеру, не назначим какую-либо
        # кнопку на «Arm» мастер-дорожки. Такое действие приведёт к ошибке.
        """установка кнопок микшера"""        
        def BE(note): return ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, note)
        mixer.set_select_buttons(BE(56),BE(54)) # выбор дорожки слева или справа от текущей
        mixer.master_strip().set_select_button(BE(94)) # прыжок на мастер-дорожку
        mixer.selected_strip().set_mute_button(BE(42)) # устанавливает кнопку заглушения звука
        mixer.selected_strip().set_solo_button(BE(44)) # устанавливает кнопку солирования
        mixer.selected_strip().set_arm_button(BE(46)) # устанавливает кнопку захвата записи
        """установка слайдеров микшера"""
        mixer.selected_strip().set_volume_control(SliderElement(MIDI_CC_TYPE, CHANNEL, 14))
        # устанавливает элемент с непрерывным значением на управление громкостью
        """Обратите внимание, что мы делим функции микшера между двумя сценариями, для того,
            чтобы сделать две сессионные рамки (одну красную и одну жёлтую),
            так что некоторые вещи мы здесь делать не будем"""
        
    def _setup_session_control(self):
        is_momentary = True
        num_tracks = 1 # единственный столбец
        num_scenes = 7 # семь строк, на которые будет назначено семь «белых» нот
        global session # Мы хотим создать экземпляр глобальной сессии как объект класса SessionComponent
        # (до сих пор он имел глобальный тип «None»)
        session = SessionComponent(num_tracks, num_scenes) # (<число дорожек>, <число сцен>)
        # Сессия подсветится («красная рамка») если установить два любых ненулевых значения
        session.set_offsets(0, 0) # (<смещение по дорожкам>, <смещение по сценам>)
        # Устанавливает начальное смещение «красной рамки» от левого верхнего угла
        """установка кнопок навигации по сессии"""
        def BE(note): return ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, note)
        next_button = BE(25); prev_button = BE(27)
        up_button = BE(51); down_button = BE(49)
        session.set_select_buttons(next_button, prev_button) # Кнопки выбора сцены — вверх и вниз
        # Мы подобным образом будем использовать и второй ControlComponent (жёлтая рамка)
        session.set_scene_bank_buttons(up_button, down_button)
        # Эти кнопки перемещают «красную рамку» вверх или вниз (сдвигают дорожки вверх или вниз, а не
        # рамку, рамка на самом деле движется в противоположную сторону)

        #right_button = BE(56); left_button = BE(54)
        #session.set_track_bank_buttons(right_button, left_button) # Перемещают «красную рамку» влево и вправо
        # Мы осуществим выбор дорожки во второй части сценария, но не здесь

        """Здесь мы определяем запуск сцен в сессии"""
        session.set_stop_all_clips_button(BE(70))
        session.selected_scene().set_launch_button(BE(30))
        launch_notes = [60, 62, 64, 65, 67, 69, 71] # это наш набор из семи «белых» нот, начиная с C4
        for index in range(num_scenes): # Число назначений кнопок запуска должно соответствовать числу сцен
            session.scene(index).set_launch_button(BE(launch_notes[index]))
            # проходит по сценам сессии и назначает им соответствующие ноты из списка launch_notes
        """Здесь мы определяем остановку дорожек в сессии"""
        # Следующий код хорош для длинных списков
        # (у нас только одна дорожка, так что мы тут переусложняем, но пригодится для будущей адаптации)
        stop_track_buttons = []
        for index in range(num_tracks):
            stop_track_buttons.append(BE(58 + index)) # здесь надо бы адаптировать к более длинному списку
            # (ведь мы уже использовали номера последующих нот в других местах)
        session.set_stop_track_clip_buttons(tuple(stop_track_buttons))
        # длина списка должна совпадать с числом дорожек в сессии (num_tracks)
        """Здесь мы определяем запуск клипов в сессии"""
        clip_launch_notes = [48, 50, 52, 53, 55, 57, 59] # это набор из семи «белых» нот, начиная с C3
        for index in range(num_scenes):
            session.scene(index).clip_slot(0).set_launch_button(BE(clip_launch_notes[index]))
            # Назначает ноту на запуск первого слота в каждой сцене
        """Здесь мы задаём микшер и полосу(ы) канала(ов), которые будут двигаться вместе с сессией"""
        session.set_mixer(mixer) # Привязывает микшер к сессии, чтобы они двигались вместе
        self.set_highlighting_session_component(session)
        # Необходимо в Live 9, чтобы появилась рамка для заданной сессии
        
    def _on_selected_track_changed(self):
        """Это переопределение добавляет специальный функционал
            (мы хотим, чтобы сессия всякий раз перемещалась к выбранной дорожке)
            Обратите внимание, что после внесения изменений в эту функцию,
            иногда необходимо перезагружать Live (не только сценарий)"""
        ControlSurface._on_selected_track_changed(self)
        # запускает on_selected_track_changed() для всех компонентов
        """как только меняется выбранная дорожка, мы устанавливаем на неё микшер и сессию"""
        selected_track = self.song().view.selected_track
        # этот метод из Live API возвращает выбранную в данный момент дорожку
        mixer.channel_strip(0).set_track(selected_track)
        all_tracks = ((self.song().tracks + self.song().return_tracks) + (self.song().master_track,))
        # это выражение позаимствовано из метода _next_track_value класса MixerComponent
        index = list(all_tracks).index(selected_track) # и это тоже
        session.set_offsets(index, session._scene_offset) # (<смещение по дорожкам>, <смещение по сценам>)
        # Мы оставляем <смещение по сценам> неизменным, но устанавливаем <смещение по дорожкам>
        # на выбранную дорожку. Это позволяет нам перебросить красную рамку на выбранную дорожку.
               
    def disconnect(self):
        """Этот метод вызывается при отключении. Здесь можно очистить все, что было использовано"""
        self.log_message(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime()) + \
                                        "--------------= ProjectX log closed =--------------") # Оставляет запись в файле журнала
        ControlSurface.disconnect(self)
        return None

А вот сценарий-напарник ProjectY (жёлтая рамка)

from __future__ import with_statement
import Live, time

""" В этом сценарии мы будем использовать только некоторые классы Фреймворка
    (остальные здесь не перечислены) """
from _Framework.ButtonElement import ButtonElement
# Класс, представляющий кнопку на контроллере
from _Framework.ChannelStripComponent import ChannelStripComponent
# Класс, подсоединяющий дорожку к микшеру
from _Framework.ClipSlotComponent import ClipSlotComponent
# Класс, представляющий слот клипа в Live
from _Framework.CompoundComponent import CompoundComponent
# Базовый класс для классов, объединяющих другие компоненты для формирования сложных компонентов
from _Framework.ControlElement import ControlElement
# Базовый класс для всех классов, представляющих элементы управления на контроллере
from _Framework.ControlSurface import ControlSurface
# Центральный базовый класс для сценариев, основанных на новом Фреймворке
from _Framework.ControlSurfaceComponent import ControlSurfaceComponent
# Базовый класс для всех классов, инкапсулирующих функции Live
from _Framework.InputControlElement import *
# Базовый класс для всех классов, представляющих элементы управления на контроллере
from _Framework.MixerComponent import MixerComponent
# Класс охватывает несколько каналов, тем самым формирует микшер
from _Framework.SceneComponent import SceneComponent
# Класс, представляющий сцену Live
from _Framework.SessionComponent import SessionComponent
# Класс охватывает несколько сцен, чтобы покрыть определённую часть сессии Live
from _Framework.SliderElement import SliderElement
# Класс, представляющий слайдер на контроллере
from _Framework.TransportComponent import TransportComponent
# Класс, инкапсулирующий все функции транспортной секции Live

CHANNEL = 0
session = None
mixer = None

class ProjectY(ControlSurface):
    __module__ = __name__
    __doc__ = " ProjectY keyboard controller script "
    
    def __init__(self, c_instance):
        ControlSurface.__init__(self, c_instance)
        with self.component_guard():
            self.log_message(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime()) + \
                                            "--------------= ProjectY log opened =--------------")
            self._set_suppress_rebuild_requests(True)
            self._setup_mixer_control()
            self._setup_session_control()
            self._set_suppress_rebuild_requests(False)

    def _setup_mixer_control(self):
        is_momentary = True
        num_tracks = 7
        global mixer
        mixer = MixerComponent(num_tracks, 0, with_eqs=False, with_filters=False)
        mixer.set_track_offset(0)
        """установка кнопок микшера"""
        self.song().view.selected_track = mixer.channel_strip(0)._track
        def BE(note): return ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, note)
        #mixer.selected_strip().set_mute_button(BE(42))
        #mixer.selected_strip().set_solo_button(BE(44))
        #mixer.selected_strip().set_arm_button(BE(46))
        track_select_notes = [36, 38, 40, 41, 43, 45, 47]
        # если увеличится число сцен (num_scenes), то в список нужно будет добавить новые ноты
        for index in range(num_tracks):
            select_button = BE(track_select_notes[index])
            mixer.channel_strip(index).set_select_button(select_button)
    
    def _setup_session_control(self):
        is_momentary = True
        num_tracks = 7
        num_scenes = 1
        global session
        session = SessionComponent(num_tracks, num_scenes)
        session.set_offsets(0, 0)
        """установка кнопок сессии"""
        def BE(note): return ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, note)
        right_button = BE(39); left_button = BE(37)
        session.set_track_bank_buttons(right_button, left_button) # Перемещают «жёлтую рамку» влево и вправо
        # Вместо этого мы будем использовать выбор дорожки микшера
        session.set_mixer(mixer)
        selected_scene = self.song().view.selected_scene # пользуемся Live API
        all_scenes = self.song().scenes
        index = list(all_scenes).index(selected_scene)
        session.set_offsets(0, index)
        self.set_highlighting_session_component(session)

    def _on_selected_scene_changed(self):
        """Это переопределение добавляет специальный функционал
            (мы хотим, чтобы сессия всякий раз перемещалась к выбранной сцене)"""
        """При внесении изменений в эту функцию, иногда необходимо
            перезагружать Live (не только сценарий)"""
        ControlSurface._on_selected_scene_changed(self)
        """как только меняется выбранная сцена, мы устанавливаем на неё микшер и сессию"""
        selected_scene = self.song().view.selected_scene
        # этот метод из Live API возвращает выбранную в данный момент сцену
        all_scenes = self.song().scenes # а этот метод возвращает все существующие сцены
        index = list(all_scenes).index(selected_scene)
        # затем определяем, где, относительно полного списка сцен, находится выбранная
        session.set_offsets(session._track_offset, index) # (<смещение по дорожкам>, <смещение по сценам>)
        # Перемещает сессию к выбранной сцене (но никак не меняет <смещение по дорожкам>)
        
    def disconnect(self):
        """очищаем всё при отключении"""
        self.log_message(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime()) + \
                                        "--------------= ProjectY log closed =--------------")
        ControlSurface.disconnect(self)
        return None

Конечно, для каждого из этих сценариев должен быть соответствующий __init__.py, выглядящий примерно так:

from ProjectX import ProjectX

def create_instance(c_instance):
    """ Создаёт и возвращает объект сценария ProjectX """
    return ProjectX(c_instance)

Чтобы использовать эти сценарии, нужно создать две папки в каталоге сценариев MIDI Remote (одну для X и одну для Y) и загрузить оба контроллера в выпадающем списке MIDI-настроек. Исходные py-файлы находятся здесь. К сожалению, я так и не смог понять, как получить две сессионные рамки в пределах одного сценария. Этим и объясняется применение «двухпапочного» подхода. Можно ещё использовать сценарии независимо, но тогда мы потеряем XY-взаимодействие. В любом случае, идея была в том, чтобы исследовать новый способ применения классов Фреймворка. Может быть данное исследование не очень полезно в реальных приложениях, но, надеюсь, описанные сценарии показали некоторую часть скрытого потенциала классов Фреймворка.

Заключение

Классы Фреймворка могут развиваться с выходом новых версий Live, и некоторые функции могут перестать работать так, как ожидалось. Однако поскольку все новые контроллеры, кажется, основываются на системе классов, вполне вероятно, что изменения будут сведены к минимуму (или, по крайней мере, стоит надеяться, что новые методы не поломают старых). Конечно, есть риск в том, что приходится использовать функции из библиотеки, которая никак не документирована. Но с другой стороны, классы Фреймворка, безусловно, помогают упростить создание сценариев.

Надеюсь, данное исследование было кому-то полезно. Вперёд, будьте творческими, радуйтесь — и делитесь своими трудами!


Автор: Ханс Петров
Март, 2010 г.

Перевод: Владимир Зевахин
Апрель, 2015 г.

Оригинальная статья на английском


Ссылки (англ.)

Благодарности

Огромнейшее спасибо участникам группы ВКонтакте Ableton Live, которые морально и материально поддержали настоящий перевод: Ивану Сидоруку, Ангелине Степаненко, Дмитрию Морозову, Suum Cuique, Dizz Martin, Борису Розумнюку, Жене Варламову, Павлу Тюкову и Сергею Козлову.

Комментариев нет:

Отправить комментарий