Ableton Midi Remote Script Framework Research - 1
原文地址:remotescripts.blogspot.tw/2010/03/introduction-to-framework-classes.htmlIntroduction to the Framework Class对于ableton midi控制脚本的简介
Backgroud——背景
(前两段略)
Live利用了Midi控制脚本为midi控制器提供了“即时映射(instant mapping)”的功能。Midi控制脚本是用python语言写的,实质上是用于转换midi数据为可控制live程序各方面的指令。Live支持的设备都会有专用的脚本文件夹和专用脚本。我们之后会讲到这些细节——现在,首先来一段小历史。
Emulation——仿真器自此已经有一段时间——那些充满好奇心和创造性的live用户发现(当然是那些不被live支持的设备)可以通过仿真一些受支持的设备来利用live的“即时映射”功能。其中最有名的仿真模型就是“Mackie emulation”。
仿真器主要用于告诉Live你已经插入了某一件midi硬件——当然实际上你没有。需要说明的是,你的硬件必须能够提供给live希望从中模拟的控制器传来的midi信息。你可以重新配置你的控制器,或者通过一个中间应用来过滤信号(例如MIDI-OX)。这种类型的仿真基本上来说是黑盒仿真,因为不需要修改midi控制脚本(而且也不需要知道脚步如何工作就可以使用)。黑盒仿真有些限制——下一步就是去研究调查脚本本身。
Script Files——脚本文件控制脚本是随ableton live一起安装的,如果你的操作系统是windows,你可以在一下目录找到它们:
C:\Program Files\Ableton live 8.x.x\Resource\MIDI Remote Scripts\
如果你的系统是Mac OS X:
/Applications/Ableton Live 9 Suite.app/Contents/App-Resources
(*打开.app文件的时候右键,选择“显示包内容(show package contents)”就可以进到下一层目录)
一个典型的MIDI控制脚步目录里会包含一些的文件夹,它们的命名类似于:
在每个文件夹,都会有一个 __init__,pyc文件,一个以控制器命名的.pyc文件,和一个或多个补充的.pyc文件。例如Vestax VCM600的文件夹中,就有以下几个文件:
- _Axiom
- _Framework
- _Generic
- _MxDcore
- _Tools
- _UserScript
- APC40
- Axiom
- AxiomPro
- etc...
最开始的几个目录是以下划线开头命名的,它们不会出现在Live的MIDI preferences control surfaces下拉菜单中(因为这些大部分是私有的辅助脚本)。其他的文件夹会包含一些受支持设备的python编译后的脚本文件(.PVC)。这是文件夹命名是用于显示在control surface下拉菜单中(更改文件夹命名的话下拉菜单不会立即刷新,需要重新启动live)。
__init__.pyc
VCM600.pyc
ViewTogglerComponent.pyc
PYC文件不是用于给人阅读的(都是一些经过编译的代码),然而人们很快发现,通过反编译这些脚本文件,可以得到能够用于分析的源代码——它提供了一个方便的地图来寻找默认的MIDI映射,和察看这些MIDI控制脚本是如何实际工作的。
Sources——源代码
Python的PYC文件是相对容易进行反编译的,而结果是PY文件也有不错的可读性——实际上,它们本质上是相同的原始源文件。
Python代码有多种方法进行反编译。Sourceforge上的Decompyle(没有拼写错,后四个字母是pyle)在2.3版本之前的运行效果都不错。在线的“depyhton”服务对于大多数的python文件也有效,然而,现今仍未有可以用于2.5版本的非商业的反编译服务。
PYC文件的版本可以在提供16进制功能的编辑器上检查前4个字节来确定。这些“magic numbers”如下:
- 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版本的python文件,而8.x.x的脚本则一般(不幸地)是2.5版本。(to be done——提供地址下载)
早期对反编译脚本的研究焦点都是在最常用到的const.py文件。这个文件在很多初代脚本中用于定义常量——包括为控制接口和MIDI音符的映射。这就不再需要去钻研MIDI执行图表,或者手动地进行MIDI音符映射赋值——这些全在文件中。修改const.py文件是一个方便的方法来调整仿真器,也有很多人从头开始创造一些新的定制脚本——一些非常复杂的脚本。
(原网页)的右边链接是一些网站——那里有很多珍贵的源代码,文档和对脚本的研究——所有这些都值得去探索。同时也出现了很多人去研究LiveAPI的运作,这是非常重要的(写脚本的“阴暗面”)。但是至今为止,未有许多对关键部分难题的研究——有ableton开发的框架类(the framework classes)。
_Framework Scripts——框架脚本
过去,写脚本似乎是一个“人人为己”的事情。OEM们若希望它们的产品可以有“即插即用”的支持想必要自已编写python脚本,这是一件有太多冗余且没有交流分享的事情。对于简单的脚本,这不算一个大问题,但是,那些有高级功能的脚本往往包含许多文件和数百万行代码。当控制器市场发展成熟,一套统一的辅助脚本的需求是显而易见的。看来Ableton公司解决这个问题的办法就是这个Framework脚本。
现在新款的控制器使用了大量(有些是独家的)框架类(Framework classes)。这个名单里面有Akai APC40,Novation Launchpad,M-Audio Axiom Pro,开源实验室的产品,Vestax VCM600,当然还有更多。这个框架脚本基本上是一个工具类——一个类和对象的模块库——用于处理那些繁重的工作,和减少对LiveAPI的直接调用。
框架类代表了LOM(Live Object Model)的“另一半”——正如Max for Live参考文档中所说名的。Max for Live的文档中对于LiveAPI中的一半有详细的叙述,也包括了对框架类(control surfaces)的简介参考。
在Max for Live文档中曝光的Component和Control(element)的命名和框架组件(Framework modules)的命名几乎一一对应。在MIDI控制脚本文件夹(这里根据类型分类)的_Framework目录下的脚本名:
Central Base Class:
ControlSurface
Control Surface Components:
ControlSurfaceComponent
TransportComponent
SessionComponent
ClipSlotComponent
ChannelStripComponent
MixerComponent
DeviceComponent
CompoundComponent
ModeSelectorComponent
SceneComponent
SessionZommingComponent
TrackEQComponent
ChannelTranslationSelector
Control Elements:
ControlElement
ButtonElement
ButtonMatrixElement
ButtonSliderElement
InputControlElement
NotifyingControlElement
PhysicalDisplayElement
SliderElement
Other Classer:
DisplayDataSource
LogicalDisplaySegment
现在是时候来探索一下这些框架脚本的内部运作了。我们通过搭建一个合适的编辑环境(开发环境)来开始。
Editing Scripts——编辑脚本
(原作者关于Python代码编写环境的描述,大概是推荐大家用IDE和一些软件的推荐——Wing IDE,Stani's Python Editor-SPE,还有搭建python语言环境)。
严格地说,要使脚本能运行的话不需要另外安装python——因为Live已经有一个内建的python编译器(如果python在脚本文件夹里发现一个.PY文件的话,它会在启动的时候来尝试编译它)。不过,也只有安装了python才能发挥使用IDE的好处(例如自动完成代码)。
虽然有过编程经验(任何语言)是一大优势,使用python的一个快速入门的方法就是play around。从一些示例代码开始(例如新建一个文件夹,复制一段简单的代码进去),试一试剪切和粘贴代码,运行调试一下,看看哪里出错。通常情况下,有错误的脚本根本不会编译,仅此而已。当然一段糟糕的脚本也有可能玩坏你的live,但直到你有足够(危险)的技术和实力为止,这是极不可能的。
Debuggin——调试Live提供了几个内建的机制来简化调试。我发现的第一个关键是利用日志文件(log file)。日志文件是一个简单的文本文件,可以在Ableton live的编号设置目录中找到:
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:
相同的日志文件还可以用于代码追查(通过Framework 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脚本时,我会经常地重新编译来检查功能性(即,以确保我没有破坏任何东西)。我通过设置控制器为“none”在MIDI preference下拉菜单,然后马上重新选择我自定义的脚本。这可以让Live重新编译修改过的脚本——无需每次都重新加载整个live程序。
现在,利用框架类来创建一个简单的走带控制器(transport)脚本,作为范例。
Example Script——示例代码
我们需要在MIDI控制脚本文件夹中新建一个文件夹,你能够任意地命名(但要注意,如果名字的开头是下划线的话,它是不会在下拉菜单中出现的)。我们命名为AAA。
然后,在这个文件夹中新建两个文件。第一个命名为__init__.py。这个文件标志着我们的目录作为一个Python包(对于编译器而言),文件包含下列几行代码:
#__init__.py
from Transport import Transport
def create_instance(c_instance):
returnTransport(c_instance)
接下来,我们创建一个Transport.py文件,这是我们主要的脚本文件(注意这个文件名不会出现在下拉菜单中——重要的只有文件夹的命名)。以下是代码:
#Transport.py
#This is a stripped-down script, which uses the Framework classes to assign MIDI notes to play, stop and record.
from _Framework.ControlSurface import ControlSurface # Central base class for scripts based on the new Framework
from _Framework.TransportComponent import TransportComponent # Class encapsulating all functions in Live's transport section
from_Framework.ButtonElement import ButtonElement # Class representing a button a the controller
classTransport(ControlSurface):
def__init__(self,c_instance):
ControlSurface.__init__(self,c_instance)
transport=TransportComponent() #Instantiate a Transport Component
transport.set_play_button(ButtonElement(True,0,0,61)) #ButtonElement(is_momentary, msg_type, channel, identifier)
transport.set_stop_button(ButtonElement(True,0,0,63))
transport.set_record_button(ButtonElement(True,0,0,66))
现在,如果我们打开live并在MIDI preferences下拉菜单中选择AAA,Live会编译我们的.PY文件,在脚本文件夹中生成对应的.PYC文件,然后运行脚本。
通道1(channel 1)上的MIDI音符60,61,63会自动地分别映射至Play,Stop和Record。当然,我们可以更据这个简单架构来进行更多的midi映射。例如,如果我们想将Tap Tempo映射至某一个键,我们只需要简单地添加一条语句:
transport.set_tap_tempo_button(ButtonElement(true, 0, 0, 68))
上面的脚本使用了基础框架模块中的一个——走带组件模块(the TransportComponent module)。除了在脚本中的三个方法,其他的公共(public)Transport Component方法包括一下内容:
- set_stop_button(button)
- set_play_button(button)
- set_seek_button(ffwd_button, rwd_button)
- set_nudge_button(up_button, down_button)
- set_record_button(button)
- set_tap_tempo_buton(button)
- set_loop_tempo_button(button)
- set_punch_buttons(in_button, out_button)
- set_metronom_button(button)
- set_overdub_button(button)
- set_tempo_control(control, fine_control)
- set_song_position_control(control)
(需要注意的是set_metronom_button在live7.x.x框架中是拼写错了的,但在8.x.x中是拼写正确的set_metronome_button。这说明了使用这个方法的脚本只能在其中一个版本运行,取决于你的拼写。。。!)
大部分的框架文件和函数(即是类和方法)的命名都是不言自明的,通过浏览这些类和方法的名称足以了解他们的作用。有的比较复杂,最好参照示例代码畸形理解。VCM600,launchpad和AxiomPro都使用了框架类,所以APC40也(理所当然)使用了。这些都是一些很有参考价值的脚本。
但使用反编译的源代码工作时,值得一提的是很多脚本都是“老派(old-school)”,预建发展的框架类。虽然它们能确实地运行,它们往往要比新的脚本更复杂。复杂性由框架类进行处理,使得大多数的脚本变得简单得多。
另一方面,有些基于框架的脚本也可以很复杂,特别当需要开发更多的类来执行特殊的功能(而框架没有提供的)。从这来看,APC40的脚本是进行复杂脚本编写的一个好示例。
现在,让我们来尝试编写脚本,干些新一代控制器能够完成的有趣功能吧(例如apc40在session界面的红框)
ProjectX
我们不需要精确地仿真APC40或者Launchpad,因为它们的功能跟硬件布局是紧紧联系在一起的——虽然要承认仿真APC40会是一个有趣的探索(这不会对大多数人有意义,除了那些apc40的用户——想要定制化它们的控制器)。相反,我们会将一个标准的midi键盘编成一个二维网格控制器,通过利用框架类。
MIDI键盘一般是一维的,而我们希望去利用它完成二维控制能做的事情。为了解决这个限制,我们使用两组按键——一组用于垂直(scene)另一组用于水平(tracks)——一个可以移动的X-Y网格按键。我们将它称为 ProjextX。
ProjextX脚本由两组键盘映射组成,可以一起或独立地使用。Part X是一个水平session component(“red box”),Part Y是一个垂直session component(“yellow box”)。我们将它保持得相对比较简单(毕竟这是用于标准MIDI键盘),但我们将用于演示如何使用一些框架类和方法——主要是Session,Mixer和Transport组件。
以下是键盘的映射表,这演示了我们将如何对这些MIDI音符进行映射。

APC40,LaunchPad和Monome都有网格按键;我们在这里将二维网格分为两个session框。红色框高度为1个track,宽度为7个scene,黄色框宽度为7个track,高度为1个scene。红色框代表以7个scene(或者为clip slot)为一组,黄色框则为7个track一组。在一起使用时,他们组成一个虚拟的7*7网格,每一个有一组独立的7个白键控制。
以下是ProjectX的脚本(红色框),脚本中使用了一些框架提供的功能:
import Live # This allows us (and the Framework methods) to use the Live API on occasion
import time # We will be using time functions for time-stamping our log file outputs
""" All of the Framework files are listed below, but we are only using using some of them in this script (the rest are commented out) """
from _Framework.ButtonElement import ButtonElement # Class representing a button a the controller
#from _Framework.ButtonMatrixElement import ButtonMatrixElement # Class representing a 2-dimensional set of buttons
#from _Framework.ButtonSliderElement import ButtonSliderElement # Class representing a set of buttons used as a slider
from _Framework.ChannelStripComponent import ChannelStripComponent # Class attaching to the mixer of a given track
#from _Framework.ChannelTranslationSelector import ChannelTranslationSelector # Class switches modes by translating the given controls' message channel
from _Framework.ClipSlotComponent import ClipSlotComponent # Class representing a ClipSlot within Live
from _Framework.CompoundComponent import CompoundComponent # Base class for classes encompasing other components to form complex components
from _Framework.ControlElement import ControlElement # Base class for all classes representing control elements on a controller
from _Framework.ControlSurface import ControlSurface # Central base class for scripts based on the new Framework
from _Framework.ControlSurfaceComponent import ControlSurfaceComponent # Base class for all classes encapsulating functions in Live
#from _Framework.DeviceComponent import DeviceComponent # Class representing a device in Live
#from _Framework.DisplayDataSource import DisplayDataSource # Data object that is fed with a specific string and notifies its observers
#from _Framework.EncoderElement import EncoderElement # Class representing a continuous control on the controller
from _Framework.InputControlElement import * # Base class for all classes representing control elements on a controller
#from _Framework.LogicalDisplaySegment import LogicalDisplaySegment # Class representing a specific segment of a display on the controller
from _Framework.MixerComponent import MixerComponent # Class encompassing several channel strips to form a mixer
#from _Framework.ModeSelectorComponent import ModeSelectorComponent # Class for switching between modes, handle several functions with few controls
#from _Framework.NotifyingControlElement import NotifyingControlElement # Class representing control elements that can send values
#from _Framework.PhysicalDisplayElement import PhysicalDisplayElement # Class representing a display on the controller
from _Framework.SceneComponent import SceneComponent # Class representing a scene in Live
from _Framework.SessionComponent import SessionComponent # Class encompassing several scene to cover a defined section of Live's session
from _Framework.SessionZoomingComponent import SessionZoomingComponent # Class using a matrix of buttons to choose blocks of clips in the session
from _Framework.SliderElement import SliderElement # Class representing a slider on the controller
#from _Framework.TrackEQComponent import TrackEQComponent # Class representing a track's EQ, it attaches to the last EQ device in the track
#from _Framework.TrackFilterComponent import TrackFilterComponent # Class representing a track's filter, attaches to the last filter in the track
from _Framework.TransportComponent import TransportComponent # Class encapsulating all functions in Live's transport section
""" Here we define some global variables """
CHANNEL = 0 # Channels are numbered 0 through 15, this script only makes use of one MIDI Channel (Channel 1)
session = None #Global session object - global so that we can manipulate the same session object from within any of our methods
mixer = None #Global mixer object - global so that we can manipulate the same mixer object from within any of our methods
class ProjectX(ControlSurface):
__module__ = __name__
__doc__ = " ProjectX keyboard controller script "
def __init__(self, c_instance):
import Live # This allows us (and the Framework methods) to use the Live API on occasion
import time # We will be using time functions for time-stamping our log file outputs
""" All of the Framework files are listed below, but we are only using using some of them in this script (the rest are commented out) """
from _Framework.ButtonElement import ButtonElement # Class representing a button a the controller
#from _Framework.ButtonMatrixElement import ButtonMatrixElement # Class representing a 2-dimensional set of buttons
#from _Framework.ButtonSliderElement import ButtonSliderElement # Class representing a set of buttons used as a slider
from _Framework.ChannelStripComponent import ChannelStripComponent # Class attaching to the mixer of a given track
#from _Framework.ChannelTranslationSelector import ChannelTranslationSelector # Class switches modes by translating the given controls' message channel
from _Framework.ClipSlotComponent import ClipSlotComponent # Class representing a ClipSlot within Live
from _Framework.CompoundComponent import CompoundComponent # Base class for classes encompasing other components to form complex components
from _Framework.ControlElement import ControlElement # Base class for all classes representing control elements on a controller
from _Framework.ControlSurface import ControlSurface # Central base class for scripts based on the new Framework
from _Framework.ControlSurfaceComponent import ControlSurfaceComponent # Base class for all classes encapsulating functions in Live
#from _Framework.DeviceComponent import DeviceComponent # Class representing a device in Live
#from _Framework.DisplayDataSource import DisplayDataSource # Data object that is fed with a specific string and notifies its observers
#from _Framework.EncoderElement import EncoderElement # Class representing a continuous control on the controller
from _Framework.InputControlElement import * # Base class for all classes representing control elements on a controller
#from _Framework.LogicalDisplaySegment import LogicalDisplaySegment # Class representing a specific segment of a display on the controller
from _Framework.MixerComponent import MixerComponent # Class encompassing several channel strips to form a mixer
#from _Framework.ModeSelectorComponent import ModeSelectorComponent # Class for switching between modes, handle several functions with few controls
#from _Framework.NotifyingControlElement import NotifyingControlElement # Class representing control elements that can send values
#from _Framework.PhysicalDisplayElement import PhysicalDisplayElement # Class representing a display on the controller
from _Framework.SceneComponent import SceneComponent # Class representing a scene in Live
from _Framework.SessionComponent import SessionComponent # Class encompassing several scene to cover a defined section of Live's session
from _Framework.SessionZoomingComponent import SessionZoomingComponent # Class using a matrix of buttons to choose blocks of clips in the session
from _Framework.SliderElement import SliderElement # Class representing a slider on the controller
#from _Framework.TrackEQComponent import TrackEQComponent # Class representing a track's EQ, it attaches to the last EQ device in the track
#from _Framework.TrackFilterComponent import TrackFilterComponent # Class representing a track's filter, attaches to the last filter in the track
from _Framework.TransportComponent import TransportComponent # Class encapsulating all functions in Live's transport section
""" Here we define some global variables """
CHANNEL = 0 # Channels are numbered 0 through 15, this script only makes use of one MIDI Channel (Channel 1)
session = None #Global session object - global so that we can manipulate the same session object from within any of our methods
mixer = None #Global mixer object - global so that we can manipulate the same mixer object from within any of our methods
class ProjectX(ControlSurface):
__module__ = __name__
__doc__ = " ProjectX keyboard controller script "
def __init__(self, c_instance):
"""everything except the '_on_selected_track_changed' override and 'disconnect' runs from here"""
ControlSurface.__init__(self, c_instance)
self.log_message(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime()) + "--------------= ProjectX log opened =--------------") # Writes message into Live's main log file. This is a ControlSurface method.
self.set_suppress_rebuild_requests(True) # Turn off rebuild MIDI map until after we're done setting up
self._setup_transport_control() # Run the transport setup part of the script
self._setup_mixer_control() # Setup the mixer object
self._setup_session_control() # Setup the session object
""" Here is some Live API stuff just for fun """
app = Live.Application.get_application() # get a handle to the App
maj = app.get_major_version() # get the major version from the App
min = app.get_minor_version() # get the minor version from the App
bug = app.get_bugfix_version() # get the bugfix version from the App
self.show_message(str(maj) + "." + str(min) + "." + str(bug)) #put them together and use the ControlSurface show_message method to output version info to console
self.set_suppress_rebuild_requests(False) #Turn rebuild back on, now that we're done setting up
def _setup_transport_control(self):
is_momentary = True # We'll only be using momentary buttons here
transport = TransportComponent() #Instantiate a Transport Component
"""set up the buttons"""
transport.set_play_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 61)) #ButtonElement(is_momentary, msg_type, channel, identifier) Note that the MIDI_NOTE_TYPE constant is defined in the InputControlElement module
transport.set_stop_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 63))
transport.set_record_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 66))
transport.set_overdub_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 68))
transport.set_nudge_buttons(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 75), ButtonElement(is_momentary, MIDI_NOTE_TYPE,CHANNEL, 73)) #(up_button, down_button)
transport.set_tap_tempo_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 78))
transport.set_metronome_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 80)) #For some reason, in Ver 7.x.x this method's name has no trailing "e" , and must be called as "set_metronom_button()"...
transport.set_loop_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 82))
transport.set_punch_buttons(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 85), ButtonElement(is_momentary, MIDI_NOTE_TYPE,CHANNEL, 87)) #(in_button, out_button)
transport.set_seek_buttons(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 90), ButtonElement(is_momentary, MIDI_NOTE_TYPE,CHANNEL, 92)) # (ffwd_button, rwd_button)
"""set up the sliders"""
transport.set_tempo_control(SliderElement(MIDI_CC_TYPE, CHANNEL, 26), SliderElement(MIDI_CC_TYPE, CHANNEL, 25)) #(control, fine_control)
transport.set_song_position_control(SliderElement(MIDI_CC_TYPE, CHANNEL, 24))
def _setup_mixer_control(self):
is_momentary = True
num_tracks = 7 #A mixer is one-dimensional; here we define the width in tracks - seven columns, which we will map to seven "white" notes
"""Here we set up the global mixer""" #Note that it is possible to have more than one mixer...
global mixer #We want to instantiate the global mixer as a MixerComponent object (it was a global "None" type up until now...)
mixer = MixerComponent(num_tracks, 2, with_eqs=True, with_filters=True) #(num_tracks, num_returns, with_eqs, with_filters)
mixer.set_track_offset(0) #Sets start point for mixer strip (offset from left)
self.song().view.selected_track = mixer.channel_strip(0)._track #set the selected strip to the first track, so that we don't, for example, try to assign a button to arm the master track, which would cause an assertion error
"""set up the mixer buttons"""
mixer.set_select_buttons(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 56),ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL,54)) #left, right track select
mixer.master_strip().set_select_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 94)) #jump to the master track
mixer.selected_strip().set_mute_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 42)) #sets the mute ("activate") button
mixer.selected_strip().set_solo_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 44)) #sets the solo button
mixer.selected_strip().set_arm_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 46)) #sets the record arm button
"""set up the mixer sliders"""
mixer.selected_strip().set_volume_control(SliderElement(MIDI_CC_TYPE, CHANNEL, 14)) #sets the continuous controller for volume
"""note that we have split the mixer functions across two scripts, in order to have two session highlight boxes (one red, one yellow), so there are a few things which we are not doing here..."""
def _setup_session_control(self):
is_momentary = True
num_tracks = 1 #single column
num_scenes = 7 #seven rows, which will be mapped to seven "white" notes
global session #We want to instantiate the global session as a SessionComponent object (it was a global "None" type up until now...)
session = SessionComponent(num_tracks, num_scenes) #(num_tracks, num_scenes) A session highlight ("red box") will appear with any two non-zero values
session.set_offsets(0, 0) #(track_offset, scene_offset) Sets the initial offset of the "red box" from top left
"""set up the session navigation buttons"""
session.set_select_buttons(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 25), ButtonElement(is_momentary, MIDI_NOTE_TYPE,CHANNEL, 27)) # (next_button, prev_button) Scene select buttons - up & down - we'll also use a second ControlComponent for this (yellow box)
session.set_scene_bank_buttons(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 51), ButtonElement(is_momentary, MIDI_NOTE_TYPE,CHANNEL, 49)) # (up_button, down_button) This is to move the "red box" up or down (increment track up or down, not screen up or down, so they are inversed)
#session.set_track_bank_buttons(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 56), ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 54)) # (right_button, left_button) This moves the "red box" selection set left & right. We'll put our track selection in Part B of the script, rather than here...
session.set_stop_all_clips_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 70))
session.selected_scene().set_launch_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 30))
"""Here we set up the scene launch assignments for the session"""
launch_notes = [60, 62, 64, 65, 67, 69, 71] #this is our set of seven "white" notes, starting at C4
for index in range(num_scenes): #launch_button assignment must match number of scenes
session.scene(index).set_launch_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, launch_notes[index])) #step through the scenes (in the session) and assign corresponding note from the launch_notes array
"""Here we set up the track stop launch assignment(s) for the session""" #The following code is set up for a longer array (we only have one track, so it's over-complicated, but good for future adaptation)..
stop_track_buttons = []
for index in range(num_tracks):
stop_track_buttons.append(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 58 + index)) #this would need to be adjusted for a longer array (because we've already used the next note numbers elsewhere)
session.set_stop_track_clip_buttons(tuple(stop_track_buttons)) #array size needs to match num_tracks
"""Here we set up the clip launch assignments for the session"""
clip_launch_notes = [48, 50, 52, 53, 55, 57, 59] #this is a set of seven "white" notes, starting at C3
for index in range(num_scenes):
session.scene(index).clip_slot(0).set_launch_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL,clip_launch_notes[index])) #step through scenes and assign a note to first slot of each
"""Here we set up a mixer and channel strip(s) which move with the session"""
session.set_mixer(mixer) #Bind the mixer to the session so that they move together
def _on_selected_track_changed(self):
"""This is an override, to add special functionality (we want to move the session to the selected track, when it changes)
Note that it is sometimes necessary to reload Live (not just the script) when making changes to this function"""
ControlSurface._on_selected_track_changed(self) # This will run component.on_selected_track_changed() for all components
"""here we set the mixer and session to the selected track, when the selected track changes"""
selected_track = self.song().view.selected_track #this is how to get the currently selected track, using the Live API
mixer.channel_strip(0).set_track(selected_track)
all_tracks = ((self.song().tracks + self.song().return_tracks) + (self.song().master_track,)) #this is from the MixerComponent's _next_track_value method
index = list(all_tracks).index(selected_track) #and so is this
session.set_offsets(index, session._scene_offset) #(track_offset, scene_offset); we leave scene_offset unchanged, but set track_offset to the selected track. This allows us to jump the red box to the selected track.
def disconnect(self):
"""clean things up on disconnect"""
self.log_message(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime()) + "--------------= ProjectX log closed =--------------") #Create entry in log file
ControlSurface.disconnect(self)
return None
然后是ProjectY的脚本(黄色框):
import Live # This allows us (and the Framework methods) to use the Live API on occasion
import time # We will be using time functions for time-stamping our log file outputs
""" We are only using using some of the Framework classes them in this script (the rest are not listed here) """
from _Framework.ButtonElement import ButtonElement # Class representing a button a the controller
from _Framework.ChannelStripComponent import ChannelStripComponent # Class attaching to the mixer of a given track
from _Framework.ClipSlotComponent import ClipSlotComponent # Class representing a ClipSlot within Live
from _Framework.CompoundComponent import CompoundComponent # Base class for classes encompasing other components to form complex components
from _Framework.ControlElement import ControlElement # Base class for all classes representing control elements on a controller
from _Framework.ControlSurface import ControlSurface # Central base class for scripts based on the new Framework
from _Framework.ControlSurfaceComponent import ControlSurfaceComponent # Base class for all classes encapsulating functions in Live
from _Framework.InputControlElement import * # Base class for all classes representing control elements on a controller
from _Framework.MixerComponent import MixerComponent # Class encompassing several channel strips to form a mixer
from _Framework.SceneComponent import SceneComponent # Class representing a scene in Live
from _Framework.SessionComponent import SessionComponent # Class encompassing several scene to cover a defined section of Live's session
from _Framework.SliderElement import SliderElement # Class representing a slider on the controller
from _Framework.TransportComponent import TransportComponent # Class encapsulating all functions in Live's transport section
""" Here we define some global variables """
CHANNEL = 0 # Channels are numbered 0 through 15, this script only makes use of one MIDI Channel (Channel 1)
session = None #Global session object - global so that we can manipulate the same session object from within our methods
mixer = None #Global mixer object - global so that we can manipulate the same mixer object from within our methods
class ProjectY(ControlSurface):
__module__ = __name__
__doc__ = " ProjectY keyboard controller script "
def __init__(self, c_instance):
ControlSurface.__init__(self, c_instance)
self.log_message(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime()) + "--------------= ProjectY log opened =--------------") # Writes message into Live's main log file. This is a ControlSurface method.
self.set_suppress_rebuild_requests(True) # Turn off rebuild MIDI map until after we're done setting up
self._setup_mixer_control() # Setup the mixer object
self._setup_session_control() # Setup the session object
self.set_suppress_rebuild_requests(False) # Turn rebuild back on, once we're done setting up
def _setup_mixer_control(self):
is_momentary = True # We use non-latching buttons (keys) throughout, so we'll set this as a constant
num_tracks = 7 # Here we define the mixer width in tracks (a mixer has only one dimension)
global mixer # We want to instantiate the global mixer as a MixerComponent object (it was a global "None" type up until now...)
mixer = MixerComponent(num_tracks, 0, with_eqs=False, with_filters=False) #(num_tracks, num_returns, with_eqs, with_filters)
mixer.set_track_offset(0) #Sets start point for mixer strip (offset from left)
"""set up the mixer buttons"""
self.song().view.selected_track = mixer.channel_strip(0)._track
#mixer.selected_strip().set_mute_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 42))
#mixer.selected_strip().set_solo_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 44))
#mixer.selected_strip().set_arm_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 46))
track_select_notes = [36, 38, 40, 41, 43, 45, 47] #more note numbers need to be added if num_scenes is increased
for index in range(num_tracks):
mixer.channel_strip(index).set_select_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, track_select_notes[index]))
def _setup_session_control(self):
is_momentary = True
num_tracks = 7
num_scenes = 1
global session #We want to instantiate the global session as a SessionComponent object (it was a global "None" type up until now...)
session = SessionComponent(num_tracks, num_scenes) #(num_tracks, num_scenes)
session.set_offsets(0, 0) #(track_offset, scene_offset) Sets the initial offset of the red box from top left
"""set up the session buttons"""
session.set_track_bank_buttons(ButtonElement(is_momentary, MIDI_NOTE_TYPE, CHANNEL, 39), ButtonElement(is_momentary, MIDI_NOTE_TYPE,CHANNEL, 37)) # (right_button, left_button) This moves the "red box" selection set left & right. We'll use the mixer track selection instead...
session.set_mixer(mixer) #Bind the mixer to the session so that they move together
selected_scene = self.song().view.selected_scene #this is from the Live API
all_scenes = self.song().scenes
index = list(all_scenes).index(selected_scene)
session.set_offsets(0, index) #(track_offset, scene_offset)
def _on_selected_scene_changed(self):
"""This is an override, to add special functionality (we want to move the session to the selected scene, when it changes)"""
"""When making changes to this function on the fly, it is sometimes necessary to reload Live (not just the script)..."""
ControlSurface._on_selected_scene_changed(self) # This will run component.on_selected_scene_changed() for all components
"""Here we set the mixer and session to the selected track, when the selected track changes"""
selected_scene = self.song().view.selected_scene #this is how we get the currently selected scene, using the Live API
all_scenes = self.song().scenes #then get all of the scenes
index = list(all_scenes).index(selected_scene) #then identify where the selected scene sits in relation to the full list
session.set_offsets(session._track_offset, index) #(track_offset, scene_offset) Set the session's scene offset to match the selected track (but make no change to the track offset)
def disconnect(self):
"""clean things up on disconnect"""
self.log_message(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime()) + "--------------= ProjectY log closed =--------------") #Create entry in log file
ControlSurface.disconnect(self)
return None
当然,每个脚本都有一个对应的__init__.py:
from ProjectX import ProjectX
def create_instance(c_instance):
""" Creates and returns the ProjectX script """
return ProjectX(c_instance)
在使用脚本的时候,两个文件夹都需要保存到MIDI Remote Script目录下(一个是X,一个是Y),而且两个控制器需要在MIDI preferences下拉菜单中选择加载。遗憾的是,我未能找到同时加载两个session框的方法,这也解释了为什么要用两个文件夹。每个脚本都可以独立使用,但这样就会失去X-Y交互。无论如何,这是去探索使用框架类的使用方法的不错的想法。也许这在实际应用中没有多大用处,但这些脚本显示了框架类有着很大潜力。
Conclusion——结论
框架类应该会随着Live的版本更新而更新,而且一些函数方法也会停止使用。不过,因为所有新的控制器脚本看来都会是基于框架类编写的,很有可能框架类的变化将保持在最低限度(至少,我们可以期望新的方法不会破坏旧的方法)。用这些未公开的函数库工作存在一定的风险,但另一方面,框架类确实能使编写脚本更加容易。
希望这种探索对其他人有所帮助。来吧,发挥你的创造性,玩得开心——和其他人分享你的工作成果!
Hanz Petrov
March 2010
没有评论:
发表评论