Вы находитесь на странице: 1из 43

Introduction to the Framework Classes Part 4 - the Final Chapter

Introduction

In Part 1 of this series, I mentioned that my journey into the world of MIDI remote scripts began with a
search for a better way of integrating my FCB1010 foot controller into my Live setup. Well, its been a fun
trip - with lots of interesting side investigations along the way - but now weve come full circle and its time
to finish up what I set out to do in the beginning. In this article, well have a look a couple of new
_Framework methods introduced in version 8.1.3, which will allow us to operate scripts in Combination
Mode, and well create a generic script which will allow the FCB1010 to work in concert with the APC40 in
fact, well set it up to emulate the APC40. And then were pretty much done. Lets start with combination
mode.

Combination Mode red and yellow and pink and green

As we saw in Part 1, set_show_highlight, a SessionComponent method, can be used to display the famous
red box, which represents the portion of a Session View which a control surface is controlling. First seen
with the APC40 and Launchpad, the red box is a must have for any clip launcher. New since 8.1.4, however,
is a functional Combination Mode specifically announced for the APC40 and APC20 which glues two or
more session highlights together. From the 8.1.4 changelog:

Combination Mode is now active when multiple Akai APC40/20s are in use. This means that the topmost
controller selected in your preferences will control tracks 1-8, the second controller selected will control
tracks 9-16, and so on.

Sounds like fun lets see how its done.

By looking through the APC sources, we can work our way back to the essential change at work here: the
SessionComponent class now has new methods called _link and _unlink (together with a new
attribute _is_linked, and a new list object known as _linked_session_instances). Linking sessions turns out to
be no more difficult than calling the SessionComponents _link method, as follows:

session = SessionComponent(num_tracks, num_scenes)


session._link()

Now, although multiple session boxes can be linked in this way, we need to manage our session offsets, if we
want our sessions to sit beside each other, and not one on top of the other. In other words, we will need to
sequentially assign non-zero offsets to our linked sessions, based on the adjacent sessions width. We'll add
the required code to the ProjectX script from Part 1, as an illustration.

First, well need a list object to hold the active instances of our ProjectX ControlSurface class, and a static
method which will be called at the end of initialisation:

_active_instances = []

def _combine_active_instances():
track_offset = 0
for instance in ProjectX._active_instances:
instance._activate_combination_mode(track_offset)
track_offset += session.width()

_combine_active_instances = staticmethod(_combine_active_instances)

We add a _do_combine call at the end of our init sequence, which in turn calls
the _combine_active_instances static method
and _combine_active_instances calls _activate_combination_mode on each instance.
def _do_combine(self):
if self not in ProjectX._active_instances:
ProjectX._active_instances.append(self)
ProjectX._combine_active_instances()

def _activate_combination_mode(self, track_offset):


if session._is_linked():
session._unlink()
session.set_offsets(track_offset, 0)
session._link()

Now that the linking is all set up, well define a _do_uncombine method, to clean things up when we
disconnect; well unlink our SessionComponent and remove ourselves from the list of active instances here.

def _do_uncombine(self):
if ((self in ProjectX._active_instances) and
ProjectX._active_instances.remove(self)):
self._session.unlink()
ProjectX._combine_active_instances()

def disconnect(self):
self._do_uncombine()
ControlSurface.disconnect(self)

So heres what we get when we load several instances of our new ProjectX script via Live's MIDI preferences
dialog:

The session highlights are all linked (with an automatic track offset for each instance, which matches the
adjacent session highlight width), and when we move any one of them, the others move along together. By
changing the offsets in _activate_combination_mode, we can get them to stack side-by-side, or one above
the other, or if we wanted to, we could indeed stack them one on top of the other. By stacking them one on
top of the other, we can control a single session highlight zone with multiple controllers which is exactly
what we want to do with the APC40 and FCB1010 in combination mode.

As of version 8.1.3, the Framework scripts have included support for session linking, but so far, the only
official scripts which implement combination mode are the APC scripts (as of 8.1.4). As shown above,
however, support for combination mode can be extended to pretty much any Framework-based script. What
we want to create then, is a script which will allow the FCB1010 to operate in combination mode together
with the APC40. Lets build one.
FCB1020 - a new script for the FCB1010

The first thing to do when setting out to create a new script is to decide on a functional layout. If we know
what were trying to achieve in terms of functionality and operation - before we touch a line of code - we
can save ourselves a good deal of time down the road. In this case, were looking for an arrangement which
will allow the FCB1010 to mirror the operation of the APC40, in so far as possible, and allow for optimized
hands-free operation.

Although the options for designing a control script are almost unlimited, generally, the resulting method of
operation needs to be intuitive. The FCB1010 has some built-in constraints, but also offers a great deal of
flexibility. We have 10 banks of 10 switches and 2 pedals to work with equivalent to 100 button controls
and 20 sliders. Interestingly, the APC40 has a similar number of buttons and knobs.

If we look at the two controllers side-by-side, a pattern emerges. Each column of the APC40s grid consists
of 5 clip launch buttons (one per scene) and 5 track control buttons (clip stop, track select, activate, solo,
& record). Each of the FCB1010s 10 banks has 5 switches on the top row, and 5 switches on the bottom row.
Based on this parallel, if we assign one FCB1010 bank to each of the APC40s track columns, the resulting
operation will indeed be intuitive, and will closely follow the APCs layout. We only have 2 pedals per bank,
however, so well map them to Track Volume, and Send A at least for now. Heres how a typical FCB1010
bank will lay out, together with the APC40 layout for comparison.

Bank 01

APC40

Well use this layout for banks 1 through 8 (since the APC40 has 8 track control columns), but because the
FCB1010 has 10 banks in total, we have 2 banks left over. Lets use bank 00 for the Master Track controls,
and the scene launch controls, in a similar arrangement to banks 1 through 8. There are no activate, solo or
record buttons for the master track, so instead, well map global play, stop and record here:

Bank 00

Now we only have one bank left - bank 09. Well use bank 09 for session and track navigation, and for device
control. Heres how it will look:

Bank 09

Although fairly intuitive, the layout described above might not suit everyones preferences. Wouldnt it be
nice if the end user could decide on his or her own preferred layout? Lives User Remote Scripts allow for
this kind of thing, so well take a similar approach. Rather than hard-coding the note and controller
mappings deep within our new script, well pull all of the assignments out into a separate file, for easy
access and editing. It will remain a python .py file (not a .txt file) - in the tradition of consts.py, of Mackie
emulation fame - but since .py files are simple text files, they can be edited using any text editor. Well call
our file MIDI_map.py. Heres a sample of what it will contain:

# General
PLAY = 7 #Global play
STOP = 8 #Global stop
REC = 9 #Global record
TAPTEMPO = -1 #Tap tempo
NUDGEUP = -1 #Tempo Nudge Up
NUDGEDOWN = -1 #Tempo Nudge Down
UNDO = -1 #Undo
REDO = -1 #Redo
LOOP = -1 #Loop on/off
PUNCHIN = -1 #Punch in
PUNCHOUT = -1 #Punch out
OVERDUB = -1 #Overdub on/off
METRONOME = -1 #Metronome on/off
RECQUANT = -1 #Record quantization on/off
DETAILVIEW = -1 #Detail view switch
CLIPTRACKVIEW = -1 #Clip/Track view switch
# Device Control
DEVICELOCK = 99 #Device Lock (lock "blue hand")
DEVICEONOFF = 94 #Device on/off
DEVICENAVLEFT = 92 #Device nav left
DEVICENAVRIGHT = 93 #Device nav right
DEVICEBANKNAVLEFT = -1 #Device bank nav left
DEVICEBANKNAVRIGHT = -1 #Device bank nav right

# Arrangement View Controls


SEEKFWD = -1 #Seek forward
SEEKRWD = -1 #Seek rewind

# Session Navigation (aka "red box")


SESSIONLEFT = 95 #Session left
SESSIONRIGHT = 96 #Session right
SESSIONUP = -1 #Session up
SESSIONDOWN = -1 #Session down
ZOOMUP = 97 #Session Zoom up
ZOOMDOWN = 98 #Session Zoom down
ZOOMLEFT = -1 #Session Zoom left
ZOOMRIGHT = -1 #Session Zoom right

# Track Navigation
TRACKLEFT = 90 #Track left
TRACKRIGHT = 91 #Track right

# Scene Navigation
SCENEUP = -1 #Scene down
SCENEDN = -1 #Scene up

# Scene Launch
SELSCENELAUNCH = -1 #Selected scene launch

Now we can easily change the layout of any of our banks, by editing this one file. In fact, pretty much
anything goes if we wanted to, we could have different layouts for each of the 10 banks, or leave some
banks unassigned, for use with guitar effects, etc. To help with layout planning, an editable PDF template
for the FCB1010 is included with the source code on the Support Files page.

Okay, so now its time to assemble the code. Since were essentially emulating the APC40 here (yes, I admit
that I was wrong in Part 2; APC40 emulation is not so crazy after all), we have a choice between starting
with the APC scripts and customizing, or building a new set of scripts which have similar functionality. Since
we wont be supporting shifted operations in our script (for the FCB1010 this would require operation with
two feet difficult to do in an upright position), we will need to make significant changes to the APC scripts.
Starting from scratch is definitely an option worth considering. On the other hand, the APC40 script will
make for a good roadmap, and while were at it, we can include some of the special features of the
APC40_22 script from Part 3 here as well.

The structure will be fairly simple. Well have an __init__.py module (to identify the directory as a python
package), a main module (called FCB1020.py), a MIDI_map.py constants file, and several special
Framework component override modules. Heres the file list (compete source code is available on the
Support Files page):

__init__.py
FCB1020.py
MIDI_Map.py
SpecialChannelStripComponent.py
SpecialMixerComponent.py
SpecialSessionComponent.py
SpecialTransportComponent.py
SpecialViewControllerComponent.py
SpecialZoomingComponent.py

We wont go into detail on the Special components, since that topic was covered in Part 3. The main module
follows the structure outlined in Part 1, but here's a quick overview. We start with the imports, and then
define the combination mode static method (as discussed above):

import Live
from _Framework.ControlSurface import ControlSurface
from _Framework.InputControlElement import *
from _Framework.SliderElement import SliderElement
from _Framework.ButtonElement import ButtonElement
from _Framework.ButtonMatrixElement import ButtonMatrixElement
from _Framework.ChannelStripComponent import ChannelStripComponent
from _Framework.DeviceComponent import DeviceComponent
from _Framework.ControlSurfaceComponent import ControlSurfaceComponent
from _Framework.SessionZoomingComponent import SessionZoomingComponent
from SpecialMixerComponent import SpecialMixerComponent
from SpecialTransportComponent import SpecialTransportComponent
from SpecialSessionComponent import SpecialSessionComponent
from SpecialZoomingComponent import SpecialZoomingComponent
from SpecialViewControllerComponent import DetailViewControllerComponent
from MIDI_Map import *

class FCB1020(ControlSurface):
__doc__ = " Script for FCB1010 in APC emulation mode "
_active_instances = []
def _combine_active_instances():
track_offset = 0
scene_offset = 0
for instance in FCB1020._active_instances:
instance._activate_combination_mode(track_offset, scene_offset)
track_offset += instance._session.width()
_combine_active_instances = staticmethod(_combine_active_instances)

Next we have our init method, where we instantiate our ControlSurface component and call the various
setup methods. We setup the session, then setup the mixer, then assign the mixer to the session, to keep
them in sync. The disconnect method follows, where we provide some cleanup for when the control surface
is disconnected:

def __init__(self, c_instance):


ControlSurface.__init__(self, c_instance)
self.set_suppress_rebuild_requests(True)
self._note_map = []
self._ctrl_map = []
self._load_MIDI_map()
self._session = None
self._session_zoom = None
self._mixer = None
self._setup_session_control()
self._setup_mixer_control()
self._session.set_mixer(self._mixer)
self._setup_device_and_transport_control()
self.set_suppress_rebuild_requests(False)
self._pads = []
self._load_pad_translations()
self._do_combine()

def disconnect(self):
self._note_map = None
self._ctrl_map = None
self._pads = None
self._do_uncombine()
self._shift_button = None
self._session = None
self._session_zoom = None
self._mixer = None
ControlSurface.disconnect(self)

The balance of the combination mode methods are next:

def _do_combine(self):
if self not in FCB1020._active_instances:
FCB1020._active_instances.append(self)
FCB1020._combine_active_instances()

def _do_uncombine(self):
if ((self in FCB1020._active_instances) and
FCB1020._active_instances.remove(self)):
self._session.unlink()
FCB1020._combine_active_instances()

def _activate_combination_mode(self, track_offset, scene_offset):


if TRACK_OFFSET != -1:
track_offset = TRACK_OFFSET
if SCENE_OFFSET != -1:
scene_offset = SCENE_OFFSET
self._session.link_with_track_offset(track_offset, scene_offset)

The session setup is based on Framework SessionComponent methods, with SessionZoomingComponent


navigation thrown in for good measure:

def _setup_session_control(self):
is_momentary = True
self._session = SpecialSessionComponent(8, 5)
self._session.name = 'Session_Control'
self._session.set_track_bank_buttons(self._note_map[SESSIONRIGHT],
self._note_map[SESSIONLEFT])
self._session.set_scene_bank_buttons(self._note_map[SESSIONDOWN],
self._note_map[SESSIONUP])
self._session.set_select_buttons(self._note_map[SCENEDN],
self._note_map[SCENEUP])
self._scene_launch_buttons = [self._note_map[SCENELAUNCH[index]] for
index in range(5) ]
self._track_stop_buttons = [self._note_map[TRACKSTOP[index]] for
index in range(8) ]
self._session.set_stop_all_clips_button(self._note_map[STOPALLCLIPS])

self._session.set_stop_track_clip_buttons(tuple(self._track_stop_buttons))
self._session.set_stop_track_clip_value(2)
self._session.selected_scene().name = 'Selected_Scene'

self._session.selected_scene().set_launch_button(self._note_map[SELSCENELAUNC
H])
self._session.set_slot_launch_button(self._note_map[SELCLIPLAUNCH])
for scene_index in range(5):
scene = self._session.scene(scene_index)
scene.name = 'Scene_' + str(scene_index)
button_row = []
scene.set_launch_button(self._scene_launch_buttons[scene_index])
scene.set_triggered_value(2)
for track_index in range(8):
button =
self._note_map[CLIPNOTEMAP[scene_index][track_index]]
button_row.append(button)
clip_slot = scene.clip_slot(track_index)
clip_slot.name = str(track_index) + '_Clip_Slot_' +
str(scene_index)
clip_slot.set_launch_button(button)
self._session_zoom = SpecialZoomingComponent(self._session)
self._session_zoom.name = 'Session_Overview'
self._session_zoom.set_nav_buttons(self._note_map[ZOOMUP],
self._note_map[ZOOMDOWN], self._note_map[ZOOMLEFT],
self._note_map[ZOOMRIGHT])

Mixer, device, and transport setup methods are similar.

def _setup_mixer_control(self):
is_momentary = True
self._mixer = SpecialMixerComponent(8)
self._mixer.name = 'Mixer'
self._mixer.master_strip().name = 'Master_Channel_Strip'

self._mixer.master_strip().set_select_button(self._note_map[MASTERSEL])
self._mixer.selected_strip().name = 'Selected_Channel_Strip'
self._mixer.set_select_buttons(self._note_map[TRACKRIGHT],
self._note_map[TRACKLEFT])
self._mixer.set_crossfader_control(self._ctrl_map[CROSSFADER])
self._mixer.set_prehear_volume_control(self._ctrl_map[CUELEVEL])

self._mixer.master_strip().set_volume_control(self._ctrl_map[MASTERVOLUME])
for track in range(8):
strip = self._mixer.channel_strip(track)
strip.name = 'Channel_Strip_' + str(track)
strip.set_arm_button(self._note_map[TRACKREC[track]])
strip.set_solo_button(self._note_map[TRACKSOLO[track]])
strip.set_mute_button(self._note_map[TRACKMUTE[track]])
strip.set_select_button(self._note_map[TRACKSEL[track]])
strip.set_volume_control(self._ctrl_map[TRACKVOL[track]])
strip.set_pan_control(self._ctrl_map[TRACKPAN[track]])
strip.set_send_controls((self._ctrl_map[TRACKSENDA[track]],
self._ctrl_map[TRACKSENDB[track]], self._ctrl_map[TRACKSENDC[track]]))
strip.set_invert_mute_feedback(True)

def _setup_device_and_transport_control(self):
is_momentary = True
self._device = DeviceComponent()
self._device.name = 'Device_Component'
device_bank_buttons = []
device_param_controls = []
for index in range(8):
device_param_controls.append(self._ctrl_map[PARAMCONTROL[index]])
device_bank_buttons.append(self._note_map[DEVICEBANK[index]])
if None not in device_bank_buttons:
self._device.set_bank_buttons(tuple(device_bank_buttons))
self._device.set_parameter_controls(tuple(device_param_controls))
self._device.set_on_off_button(self._note_map[DEVICEONOFF])
self._device.set_bank_nav_buttons(self._note_map[DEVICEBANKNAVLEFT],
self._note_map[DEVICEBANKNAVRIGHT])
self._device.set_lock_button(self._note_map[DEVICELOCK])
self.set_device_component(self._device)

detail_view_toggler = DetailViewControllerComponent()
detail_view_toggler.name = 'Detail_View_Control'

detail_view_toggler.set_device_clip_toggle_button(self._note_map[CLIPTRACKVIE
W])

detail_view_toggler.set_detail_toggle_button(self._note_map[DETAILVIEW])

detail_view_toggler.set_device_nav_buttons(self._note_map[DEVICENAVLEFT],
self._note_map[DEVICENAVRIGHT] )

transport = SpecialTransportComponent()
transport.name = 'Transport'
transport.set_play_button(self._note_map[PLAY])
transport.set_stop_button(self._note_map[STOP])
transport.set_record_button(self._note_map[REC])
transport.set_nudge_buttons(self._note_map[NUDGEUP],
self._note_map[NUDGEDOWN])
transport.set_undo_button(self._note_map[UNDO])
transport.set_redo_button(self._note_map[REDO])
transport.set_tap_tempo_button(self._note_map[TAPTEMPO])
transport.set_quant_toggle_button(self._note_map[RECQUANT])
transport.set_overdub_button(self._note_map[OVERDUB])
transport.set_metronome_button(self._note_map[METRONOME])
transport.set_tempo_control(self._ctrl_map[TEMPOCONTROL])
transport.set_loop_button(self._note_map[LOOP])
transport.set_seek_buttons(self._note_map[SEEKFWD],
self._note_map[SEEKRWD])
transport.set_punch_buttons(self._note_map[PUNCHIN],
self._note_map[PUNCHOUT])

Weve also included a DetailViewComponent above, which communicates session view changes via the Live
API. Next is _on_selected_track_changed, a ControlSurface class method override, which keeps the selected
tracks device in focus. And for drum rack note mapping, weve included a _load_pad_translationsmethod,
which adds x and y offsets to the Drum Rack note and channel assignments, which are set in
the MIDI_map.py file. This allows us to pass the translations array as an argument to the
ControlSurface set_pad_translations method in the expected format.

def _on_selected_track_changed(self):
ControlSurface._on_selected_track_changed(self)
track = self.song().view.selected_track
device_to_select = track.view.selected_device
if device_to_select == None and len(track.devices) > 0:
device_to_select = track.devices[0]
if device_to_select != None:
self.song().view.select_device(device_to_select)
self._device_component.set_device(device_to_select)

def _load_pad_translations(self):
if -1 not in DRUM_PADS:
pad = []
for row in range(4):
for col in range(4):
pad = (col, row, DRUM_PADS[row*4 + col], PADCHANNEL,)
self._pads.append(pad)
self.set_pad_translations(tuple(self._pads))

Finally, we have _load_MIDI_map. Here, we create a list of ButtonElements and a list of SliderElements.
When we make mapping assignments in our MIDI_map.py file, we are actually indexing objects from these
lists. By instantiating the ButtonElements and SliderElements as independent objects, we limit the risk of
duplicate MIDI assignments, which would prevent our script from loading. Any particular MIDI note/channel
message from a control surface can only be assigned to a single InputControlElement (such as a button or
slider), however, an InputControlElement can be used more than once, with different components. This
setup also allows us to append None to the end of each list, so that null assignments can be specified in the
MIDI_map file, by using -1 in place of a note number (in python, [-1] corresponds to the last element of a
list).

def _load_MIDI_map(self):
is_momentary = True
for note in range(128):
button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, NOTECHANNEL,
note)
button.name = 'Note_' + str(note)
self._note_map.append(button)
self._note_map.append(None) #add None to the end of the list,
selectable with [-1]
for ctrl in range(128):
control = SliderElement(MIDI_CC_TYPE, CTRLCHANNEL, ctrl)
control.name = 'Ctrl_' + str(ctrl)
self._ctrl_map.append(control)
self._ctrl_map.append(None)

Now, speaking of MIDI assignments, since all of our mappings are editable, and grouped in a separate file,
couldnt we use our script with just about any control surface, and not only the FCB1010? Yes, indeed we
could.

Generic APC Emulation

Our new FCB10120 script can be used as a generic APC emulator, since it merely maps MIDI Note and CC
input to specific _Framework component functions, mimicking the APC script setup. In fact, none of this is
very different from the User script mechanism provided by Ableton - although our script has a few extra
features that the User script does not support, including Session Highlighting (aka red box), and the ability
to work in combination mode, with an APC40, or with another instance of itself, or with any other controller
which supports combination mode.

Some setup is required, however, to accommodate alternate controller configurations. These configurations
could be alternate configurations for the same control surface, or alternate configurations for different
control surfaces.

Probably the simplest way of setting up an alternate configuration, is to create one or more copies of the
FCB1020 script folder, modify the assignments in the MIDI_map.py file as required, and then re-name the
directory to suit. The new folder will be selectable by name from Lives control surface drop-down list, the
next time Live is started. This way, one could have,
say, FCB1020_device_mode and FCB1020_transport_mode as separate configurations, listed one above the
other in the control surface drop-down. Note however, that one should avoid leading underscores in folder
name, unless a folder is intended to be hidden.

Another way to accommodate alternate setups would be to reprogram the control surface itself where this
is possible - to match the note and CC mappings found in the MIDI_map.py file. Depending on the control
surface, this could be done manually, with stand-alone software, or by using Lives dump button from the
preferences dialog (in fact, a sysex file for the FCB1010 is included with the support files package which
accompanies this article, for this purpose).

Of course, our script can also be used as a generic APC emulator for multiple controllers at the same time.
This can be done a number of ways, including:
1) Daisy-chain several control surfaces using MIDI Thru ports;
2) Load the script multiple times, using multiple slots;
3) Create multiple copies of the script folder, rename to suit, and load into different slots.

For multiple control surfaces which use different MIDI channels, separate instances of the script would need
to be loaded, from separate folders, with the channel assignments in the MIDI_maps.py modified to suit in
each folder.

And getting back to our original design setup, we can see that the FCB1010 and the APC40 now work well
together in Combination Mode, and the FCB1010 is able to control of most of the APCs functions within
one session highlight area, and without loss of focus. We have included a good deal of flexibility in our script
too, so we can easily modify the various bank layouts to suit our needs, as they develop and change.

Conclusion

In this new era of multi-touch controllers, its nice to know that a sturdy old workhorse like the FCB1010 still
has a place in our arsenal of control surfaces - and that it works well in combination with the soon-to-be-a-
classic and not-yet-obsolete APC40. As for MIDI remote scripts, theyre still at the heart of all control surface
communications with the Live API - and the _Framework classes have been holding their own for quite a
while now, with interesting new methods being added from time to time. Hopefully, this series of articles
has been useful, and will encourage others to share their findings with the Live community. Happy scripting.

Hanz Petrov
September 7, 2010

hanz.petrov
at gmail.com
Introduction to the Framework Classes Part 2
Background

In this post, well have a look at the differences between Live 7 and Live 8 remote scripts, get the Max for
Live point-of-view on control surfaces, take a detailed look at the newest APC40 (and APC20) scripts and
demonstrate a few APC script hacks along the way. If youre new to MIDI remote scripts, you might want to
have a look at Part 1 of the Introduction to the Framework Classes before coming back here for Part 2.

Keeping up with recent changes

As discussed previously, most of what we know about MIDI remote scripting has been based on exploring
decompiled Python pyc files. The Live 7 remote scripts are Python 2.2 files, and have proven to be relatively
easy to decompile. Live 8s integration of Python 2.5, on the other hand, presents a new challenge. At
present, there is no reliable, freely accessible method for decompiling 2.5 pyc files. It is possible, however,
to get a good sense of the latest changes made to the MIDI remote scripts using the tools at hand.

Unpyc is one such tool, and unpyc can decompile python 2.5 files - but only up to a point. In most cases, it
will only produce partial source code, but at least it lets us know where it is having trouble. It can, however,
disassemble 2.5 files without fail. When were armed with a partial decompile, and a complete disassembly,
it is possible to reconstruct working script source code - although the process is tedious at the best of times.
But even without reconstructing complete sources, Unpyc allows us to take a peek behind the scenes and
understand the nature of the changes implemented with the most recent MIDI remote scripts.

As an example, here is the mixer setup method from the APC40.py script - Live 8.1.1 version:

def _setup_mixer_control(self):
is_momentary = True
mixer = SpecialMixerComponent(8)
mixer.name = 'Mixer'
mixer.master_strip().name = 'Master_Channel_Strip'
mixer.selected_strip().name = 'Selected_Channel_Strip'
for track in range(8):
strip = mixer.channel_strip(track)
strip.name = 'Channel_Strip_' + str(track)
volume_control = SliderElement(MIDI_CC_TYPE, track, 7)
arm_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, track,
48)
solo_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, track,
49)
mute_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, track,
50)
select_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE,
track, 51)
volume_control.name = str(track) + '_Volume_Control'
arm_button.name = str(track) + '_Arm_Button'
solo_button.name = str(track) + '_Solo_Button'
mute_button.name = str(track) + '_Mute_Button'
select_button.name = str(track) + '_Select_Button'
strip.set_volume_control(volume_control)
strip.set_arm_button(arm_button)
strip.set_solo_button(solo_button)
strip.set_mute_button(mute_button)
strip.set_select_button(select_button)
strip.set_shift_button(self._shift_button)
strip.set_invert_mute_feedback(True)
crossfader = SliderElement(MIDI_CC_TYPE, 0, 15)
master_volume_control = SliderElement(MIDI_CC_TYPE, 0, 14)
master_select_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0,
80)
prehear_control = EncoderElement(MIDI_CC_TYPE, 0, 47,
Live.MidiMap.MapMode.relative_two_compliment)
crossfader.name = 'Crossfader'
master_volume_control.name = 'Master_Volume_Control'
master_select_button.name = 'Master_Select_Button'
prehear_control.name = 'Prehear_Volume_Control'
mixer.set_crossfader_control(crossfader)
mixer.set_prehear_volume_control(prehear_control)
mixer.master_strip().set_volume_control(master_volume_control)
mixer.master_strip().set_select_button(master_select_button)
return mixer

Compare the above with the equivalent code from the 7.0.18 version:

def _setup_mixer_control(self):
is_momentary = True
mixer = MixerComponent(8)
for track in range(8):
strip = mixer.channel_strip(track)
strip.set_volume_control(SliderElement(MIDI_CC_TYPE, track, 7))
strip.set_arm_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE,
track, 48))
strip.set_solo_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE,
track, 49))
strip.set_mute_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE,
track, 50))
strip.set_select_button(ButtonElement(is_momentary,
MIDI_NOTE_TYPE, track, 51))
strip.set_shift_button(self._shift_button)
strip.set_invert_mute_feedback(True)
mixer.set_crossfader_control(SliderElement(MIDI_CC_TYPE, 0, 15))
mixer.set_prehear_volume_control(EncoderElement(MIDI_CC_TYPE, 0, 47,
Live.MidiMap.MapMode.relative_two_compliment))
mixer.master_strip().set_volume_control(SliderElement(MIDI_CC_TYPE,
0, 14))
mixer.master_strip().set_select_button(ButtonElement(is_momentary,
MIDI_NOTE_TYPE, 0, 80))
return mixer

As we can see, the main difference between the Live 7.0.18 code and the Live 8.1.1 code is that in the new
script, name attributes have been assigned to most of the components and elements. So who needs names?
Max for Live needs names.

Max for Live keeping up with Cycling 74

Max for Live requires Live 8.1 or higher, and, accordingly, new Live 8 versions include Max-compatible MIDI
remote scripts. The name attributes assigned to the python objects and methods allow Max for Live to see
and access python methods and objects more easily (objects including control surfaces, components, and
control elements). This can be verified with the help of Max for Lives LiveAPI resource patches
(M4L.api.SelectComponent, M4L.api.SelectControlSurface, M4L.api.SelectControl, etc.). Before we
demonstrate, however, lets have a look at how Control Surfaces fit into the world of Maxs Live Object
Model (LOM).
The three main root objects in the Live Object Model are known as live_app (Application), live_set (Song),
and control_surfaces (Control Surface). Cycling 74 provides somewhat detailed documentation for the first
two, however, the last one seems to have been left out - intentionally, no doubt. Based on the LOM diagram,
we can see that the Control Surfaces root object includes classes for components and controls. These map
directly to python Framework modules (modules with names
like ControlSurface, TransportComponent, SessionComponent, ButtonElement, etc.). Now, although
documentation which explains this is almost non-existent, it turns out that the python methods and objects
of the Framework classes are both visible and accessible from Max for Live. The remote script methods
appear to Max as functions functions which can be called using the syntax and arguments defined in the
Python scripts.

It is now clear that in order to gain a proper understanding of how to manipulate more complicated Control
Surfaces, a study of the python remote scripts is essential - because that's where the Control Surface
functions originate. We'll be demonstrating this link between Max for Live and the python scripts, but before
we do, lets take a peek under the hood of what has proven to be a very popular Live controller -
the APC40 (now almost a year old).

The APC40 under the hood

The latest Live 7 versions include MIDI remote scripts for the APC40, and, of course, Live 8.1 and higher also
support the APC40. As mentioned above, the Live 8.1 versions of the APC40 scripts show slight differences -
generally, name attributes have been added to most of the methods and objects. Well base our
investigation here on the 8.1.1 scripts, in an effort to stay somewhat current.

There are 11 files in the APC40 MIDI remote scripts directory:

__init__.pyc
APC40.pyc
DetailViewControllerComponent.pyc
EncoderMixerModeSelectorComponent.pyc
PedaledSessionComponent.pyc
ShiftableDeviceComponent.pyc
ShiftableTranslatorComponent.pyc
ShiftableTransportComponent.pyc
SpecialChannelStripComponent.pyc
SpecialMixerComponent.pyc
RingedEncoderElement.pyc

The __init__.py script is rather uninteresting, as it is with most scripts:

import Live
from APC40 import APC40
def create_instance(c_instance):
""" Creates and returns the APC40 script """
return APC40(c_instance)

This is a standard init, as shown in Part 1 of the Introduction to Remote Scripts. Now, the script files here
with the longish names are special classes, which inherit from Framework modules, but add custom
functionality. What they do (and the name of the Framework classes they inherit from) is described in the
Docstrings of the scripts themselves:

class DetailViewControllerComponent(ControlSurfaceComponent):
' Component that can toggle the device chain- and clip view of the
selected track '
class EncoderMixerModeSelectorComponent(ModeSelectorComponent):
' Class that reassigns encoders on the AxiomPro to different mixer
functions '
class PedaledSessionComponent(SessionComponent):
' Special SessionComponent with a button (pedal) to fire the selected
clip slot '
class RingedEncoderElement(EncoderElement):
' Class representing a continuous control on the controller enclosed with
an LED ring '
class ShiftableDeviceComponent(DeviceComponent):
' DeviceComponent that only uses bank buttons if a shift button is
pressed '
class ShiftableTranslatorComponent(ChannelTranslationSelector):
' Class that translates the channel of some buttons as long as a shift
button is held '
class ShiftableTransportComponent(TransportComponent):
' TransportComponent that only uses certain buttons if a shift button is
pressed '

It is interesting to note that the EncoderMixerModeSelectorComponent module appears to have been


recycled from the AxiomPro script. And, for anyone interested, the rest of the python code can be
examined here.

The most interesting module of the lot by far, is APC40.py - which is where most of the action is. This file
begins with the imports:

import Live
from _Framework.ControlSurface import ControlSurface
from _Framework.InputControlElement import *
from _Framework.SliderElement import SliderElement
from _Framework.ButtonElement import ButtonElement
from _Framework.EncoderElement import EncoderElement
from _Framework.ButtonMatrixElement import ButtonMatrixElement
from _Framework.MixerComponent import MixerComponent
from _Framework.ClipSlotComponent import ClipSlotComponent
from _Framework.ChannelStripComponent import ChannelStripComponent
from _Framework.SceneComponent import SceneComponent
from _Framework.SessionZoomingComponent import SessionZoomingComponent
from _Framework.ChannelTranslationSelector import ChannelTranslationSelector
from EncoderMixerModeSelectorComponent import
EncoderMixerModeSelectorComponent
from RingedEncoderElement import RingedEncoderElement
from DetailViewControllerComponent import DetailViewControllerComponent
from ShiftableDeviceComponent import ShiftableDeviceComponent
from ShiftableTransportComponent import ShiftableTransportComponent
from ShiftableTranslatorComponent import ShiftableTranslatorComponent
from PedaledSessionComponent import PedaledSessionComponent
from SpecialMixerComponent import SpecialMixerComponent

As expected, in addition to the Live import (which provides direct access to the Live API), and the special
modules listed previously, all of the other imports are _Framework modules. Again, see Part 1 for more
detail.

Next are the constants, which are used in the APC40 sysex exchange (more on this later):

SYSEX_INQUIRY = (240, 126, 0, 6, 1, 247)


MANUFACTURER_ID = 71
PRODUCT_MODEL_ID = 115
APPLICTION_ID = 65
CONFIGURATION_ID = 1

Then, after the name and docstring, we have the obligatory __init__ method. The APC40 __init__ looks like
this:

def __init__(self, c_instance):


ControlSurface.__init__(self, c_instance)
self.set_suppress_rebuild_requests(True)
self._suppress_session_highlight = True
is_momentary = True
self._shift_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0,
98)
self._shift_button.name = 'Shift_Button'
self._suggested_input_port = 'Akai APC40'
self._suggested_output_port = 'Akai APC40'
session = self._setup_session_control()
mixer = self._setup_mixer_control()
self._setup_device_and_transport_control()
self._setup_global_control(mixer)
session.set_mixer(mixer)
for component in self.components:
component.set_enabled(False)
self.set_suppress_rebuild_requests(False)
self._device_id = 0
self._common_channel = 0
self._dongle_challenge = (Live.Application.get_random_int(0,
2000000), Live.Application.get_random_int(2000001, 4000000))

As we can see, standard Framework-based scripting is used to create a session object, a mixer, device and
transport components, and global controls, and then to assign the mixer to the session. Theres nothing very
mysterious here except perhaps _dongle_challenge. And - you may be wondering - where does the
infamous secret handshake live? Why, in handle_sysex of course.

The Secret Handshake not so secret anymore

Within days of the APC40 hitting retail shelves, it was discovered that the secret handshake is based on a
sysex exchange. But how does it work exactly, and what is it hiding? Its not hiding anything that cant be
done with basic Framework scripting (as we saw in Part 1), and it works by sending a dongle challenge
sysex string, then looking for a correct response from the controller. If the response from the controller
matches the expected response (i.e. the handshake succeeds), then all of the controls on the controller are
enabled and the session highlight (aka red box) is turned on. Heres part of the handle_sysex method, in
native python:

def handle_sysex(self, midi_bytes):


if ((midi_bytes[3] == 6) and (midi_bytes[4] == 2)):
assert (midi_bytes[5] == MANUFACTURER_ID)
assert (midi_bytes[6] == PRODUCT_MODEL_ID)
version_bytes = midi_bytes[9:13]
self._device_id = midi_bytes[13]
self._send_midi((240,
MANUFACTURER_ID,
self._device_id,
PRODUCT_MODEL_ID,
96,
0,
4,
APPLICTION_ID,
self.application().get_major_version(),
self.application().get_minor_version(),
self.application().get_bugfix_version(),
247))
challenge1 = [0,0,0,0,0,0,0,0]
challenge2 = [0,0,0,0,0,0,0,0]
#...
dongle_message = ((((240,
MANUFACTURER_ID,
self._device_id,
PRODUCT_MODEL_ID,
80,
0,
16) + tuple(challenge1)) +
tuple(challenge2)) + (247))
self._send_midi(dongle_message)
message = ((('APC40: Got response from controller, version ' +
str(((version_bytes[0] << 4) + version_bytes[1]))) + '.') +
str(((version_bytes[2] << 4) + version_bytes[3])))
self.log_message(message)
elif (midi_bytes[4] == 81):
assert (midi_bytes[1] == MANUFACTURER_ID)
assert (midi_bytes[2] == self._device_id)
assert (midi_bytes[3] == PRODUCT_MODEL_ID)
assert (midi_bytes[5] == 0)
assert (midi_bytes[6] == 16)
response = [long(0),
long(0)]
#...
expected_response =
Live.Application.encrypt_challenge(self._dongle_challenge[0],
self._dongle_challenge[1])
if ((long(expected_response[0]) == response[0]) and
(long(expected_response[1]) == response[1])):
self._suppress_session_highlight = False
for component in self.components:
component.set_enabled(True)
self._on_selected_track_changed()

The above sysex usage matches that described in the Akai APC40 Communications Protocol document. Well,
the standard MMC Device Enquiry and response parts do anyway the dongle part is not documented. As we
can see, some of this exchange involves setting the APC40 into Ableton Mode and identifying the host
application to the APC40 as Live complete with major, minor, and bugfix version info.

Although the APC40 handshake is effective at discouraging casual emulation, it is not clear what exactly is
being protected here, since any script can make of use the Framework SessionComponent module (or the
Live API) to get the infamous red box to display. On the other hand, who in their right mind would want to
emulate a hard-wired non-programmable MIDI controller with an 8x5 grid of LED buttons, 16 rotary
controllers, a cross-fader, and 9 sliders unless theyre someone who already owns an APC40?! I suspect that
the majority of Monome owners probably wouldnt be interested in using their high-end boutique hardware
to emulate a mass-market controller like the APC40, and besides, theyve already got a wealth of open-
source software applications and scripts to play with. So the real mystery is: why the dongle?

In any event, bypassing the handshake is simply a matter of overriding the handle_sysex method. Other than
for Mode initialization, sysex is not an important part of the APC40 scripts.
Now, for a change of pace, lets have a quick look at how we can manipulate the APC40s LEDs using a
remote script. Well set up a little light show with some python code, and then call it up from Max.

APC40 Lights & Magic

As described in the APC40 Communications Protocol document, the various LEDs on the APC40 (around 380 of
them according to Akai - and hence the need for a separate power supply) can be manipulated via MIDI
messages sent to the controller. No sysex voodoo here simple note on and note off commands is all it
takes.

The LEDs are turned on with MIDI note-on messages. The first byte of the message is the note-on part, with
the MIDI channel bits used to identify the track number (for those LEDs which are associated with tracks).
Individual LEDs are identified using the second byte (the note number), and the third byte is used to set the
colour and state of the LED. The APC40 note map is a useful reference here.

The Communications Protocol document lists the following possible colours and states for the clip launch
LEDs:

0=off, 1=green, 2=green blink, 3=red, 4=red blink, 5=yellow, 6=yellow blink, 7-
127=green

In the _setup_session_control method of the APC40 python script, these are assigned as values:

clip_slot.set_started_value(1)
clip_slot.set_triggered_to_play_value(2)
clip_slot.set_recording_value(3)
clip_slot.set_triggered_to_record_value(4)
clip_slot.set_stopped_value(5)

The values in the python script match the colours and states listed in the Communications Protocol
document, although only 5 are assigned above. If we wanted to change the 8x5 grid colour assignments (if
we had colour vision deficiency, for example), wed need to override this part of the script.

Now, based on this information, well set up a random pattern of colours - using the Live API to generate the
random values and then get the colours to scroll down the rows of the matrix. Before we can call our new
functions, however, well need a MIDI remote script to put them in. Although we could add the new code
straight into the APC40 script (by modifying the source code), instead, lets create a new script which
inherits from the APC40 class. Well call our new control surface script APC40plus1.

Heres the new _init__.py code for our new script:

from APC40plus1 import APC40plus1


def create_instance(c_instance):
return APC40plus1(c_instance)

And heres the code for our new APC40plus1 class, complete with lightshow methods:

# http://remotescripts.blogspot.com

from APC40.APC40 import *

class APC40plus1(APC40):
__module__ = __name__
__doc__ = ' Main class derived from APC40 '
def __init__(self, c_instance):
APC40.__init__(self, c_instance)
self.show_message("APC40plus1 script loaded")
self.light_loop()

def light_loop(self):
#self.name = 'light_loop'
for index in range (4, 105, 4): #start lights in 4 ticks; end after
104; step in 4 tick increments
self.schedule_message(index, self.lights_on) #turn lights on
for index in range (108, 157, 4): #start turning off after 108 ticks;
end after 156; step in 4 tick increments
self.schedule_message(index, self.lights_off) #turn lights off
self.schedule_message(156, self.refresh_state) #refresh the
controller to turn clip lights back on

def lights_on(self):
for col_num in range (8): #load random colour numbers into the buffer
row (row 0)
colour = Live.Application.get_random_int(0, 10) #0=off, 1=green,
2=green blink, 3=red, 4=red blink, 5=yellow, 6=yellow blink, 7-127=green
if colour % 2 == 0 or colour > 6: #filter out the blinking lights
(even numbers) and skew towards having more "off" lights
colour = 0
list_of_rows[0][col_num] = colour #load the buffer row
self.load_leds()

def lights_off(self):
for col_num in range (8): #step through 8 columns/tracks/channels
list_of_rows[0][col_num] = 0 #set to zero (lights off)
self.load_leds()

def load_leds(self):
for row_num in range (6, 0, -1): #the zero row is a buffer, which
gets loaded with a random sequence of colours
note = 52 + row_num #set MIDI notes to send to APC, starting at
53, which is the first scene_launch button
if row_num == 6: #the clip_stop row is out of sequence with the
grid
note = 52 #note number for clip_stop row
for col_num in range (8): #8 columns/tracks/channels
list_of_rows[row_num][col_num] = list_of_rows[row_num-
1][col_num] #load each slot from the preceding row
status_byte = col_num #set channel part of status_byte
status_byte += 144 #add note_on to channel number
self._send_midi((status_byte, note,
list_of_rows[row_num][col_num])) #for APC LEDs, send (status_byte, note,
colour)

list_of_rows = [[0]*8 for index in range(7)] #create an 8 x 7 array of zeros;


8 tracks x (1 buffer row + 5 scene rows + 1 clip stop row)

With these two files saved to a new MIDI remote scripts folder, we can select our new controller script in the
MIDI preferences drop-down. This will force Live to compile and run the new script. If the red box doesnt
show up, we know that something is wrong, and we can check the Live log file for errors, and trouble-shoot
from there. Weve set up our light show to run on initialization, so it should run immediately, but what we
really wanted to demonstrate is that the our functions can be called from Max for Live.

For this, well create a simple Max patch which uses the M4L.api.SelectControlSurface resource patch to get
a path to our new function, so that we can bang it with the function call. We send a call light_loopmessage
to the control script, which in turn calls lightshow_on to turns the lights on, and lightshow_off to turn the
lights back off - and resets the control surface, so that the LEDs reflect session view state.

And here's the result:

Well, although that was a neat diversion, lets try to do something more useful here well re-assign the
Metronome button to act as a Device Lock button. Well do this using our same APC40plus1 script, together
with some method overrides (and without the help of Max for Live this time).

Device Lock why isnt this feature standard?

The Framework TransportComponent class contains both of the methods which interest us here:
set_metronome_button and set_device_lock_button. These are inherited by the APC40 class, and can be
used to change the button assignments. In order to change one for the other, we need to override the APC40
script method where they are normally assigned. This happens in
the _setup_device_and_transport_control section of code. Before we override the method, however, well
need to do some basic setup.
First, we instantiate an APC40 ControlSurface object, then we initialize using the APC40s __init__ method,
and finally we override the _setup_device_and_transport_control method, which is where we assign the
physical metronome button to act as a device lock button:

# http://remotescripts.blogspot.com

from APC40.APC40 import *

class APC40plus1(APC40):
__module__ = __name__
__doc__ = ' Main class derived from APC40 '
def __init__(self, c_instance):
APC40.__init__(self, c_instance)
self.show_message("APC40plus1 script loaded")

def _setup_device_and_transport_control(self): #overriden so that we can


to reassign the metronome button to device lock
is_momentary = True
device_bank_buttons = []
device_param_controls = []
bank_button_labels = ('Clip_Track_Button', 'Device_On_Off_Button',
'Previous_Device_Button', 'Next_Device_Button', 'Detail_View_Button',
'Rec_Quantization_Button', 'Midi_Overdub_Button', 'Device_Lock_Button')
for index in range(8):
device_bank_buttons.append(ButtonElement(is_momentary,
MIDI_NOTE_TYPE, 0, 58 + index))
device_bank_buttons[-1].name = bank_button_labels[index]
ring_mode_button = ButtonElement(not is_momentary, MIDI_CC_TYPE,
0, 24 + index)
ringed_encoder = RingedEncoderElement(MIDI_CC_TYPE, 0, 16 +
index, Live.MidiMap.MapMode.absolute)
ringed_encoder.set_ring_mode_button(ring_mode_button)
ringed_encoder.name = 'Device_Control_' + str(index)
ring_mode_button.name = ringed_encoder.name + '_Ring_Mode_Button'
device_param_controls.append(ringed_encoder)
device = ShiftableDeviceComponent()
device.name = 'Device_Component'
device.set_bank_buttons(tuple(device_bank_buttons))
device.set_shift_button(self._shift_button)
device.set_parameter_controls(tuple(device_param_controls))
device.set_on_off_button(device_bank_buttons[1])
device.set_lock_button(device_bank_buttons[7]) #assign device lock to
bank_button 8 (in place of metronome)...
self.set_device_component(device)
detail_view_toggler = DetailViewControllerComponent()
detail_view_toggler.name = 'Detail_View_Control'
detail_view_toggler.set_shift_button(self._shift_button)

detail_view_toggler.set_device_clip_toggle_button(device_bank_buttons[0])
detail_view_toggler.set_detail_toggle_button(device_bank_buttons[4])
detail_view_toggler.set_device_nav_buttons(device_bank_buttons[2],
device_bank_buttons[3])
transport = ShiftableTransportComponent()
transport.name = 'Transport'
play_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0, 91)
stop_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0, 92)
record_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0, 93)
nudge_up_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0, 100)
nudge_down_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0,
101)
tap_tempo_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0, 99)
play_button.name = 'Play_Button'
stop_button.name = 'Stop_Button'
record_button.name = 'Record_Button'
nudge_up_button.name = 'Nudge_Up_Button'
nudge_down_button.name = 'Nudge_Down_Button'
tap_tempo_button.name = 'Tap_Tempo_Button'
transport.set_shift_button(self._shift_button)
transport.set_play_button(play_button)
transport.set_stop_button(stop_button)
transport.set_record_button(record_button)
transport.set_nudge_buttons(nudge_up_button, nudge_down_button)
transport.set_tap_tempo_button(tap_tempo_button)
transport.set_quant_toggle_button(device_bank_buttons[5])
transport.set_overdub_button(device_bank_buttons[6])
#transport.set_metronome_button(device_bank_buttons[7]) #using this
button for lock to device instead...
bank_button_translator = ShiftableTranslatorComponent()

bank_button_translator.set_controls_to_translate(tuple(device_bank_buttons))
bank_button_translator.set_shift_button(self._shift_button)

Now, we save our new APC40plus1 script into its MIDI Remote Scripts directory, fire up Live, and select
APC40plus1 from the MIDI Control Surfaces drop-down.

Voil, the Metronome button is now a Device Lock button (sorry, no video - but trust me - it does work). The
python code for this mod (together with the lightshow) can be downloaded here. Now, although weve made
functional changes without modifying the original APC40 scripts, were still dependent on those original
scripts, because we inherit methods from them. This means that well need to watch out for MIDI remote
script changes in new versions of Live, since they might break our new script. So why dont we get a head
start by checking out compatibility with 8.1.3 RC-1?

Live 8.1.3 subtle changes

As Live 8.1.3 enters the Release Candidate phase, we can take a sneak peek at the close-to-final MIDI
remote scripts included with the beta release. It turns out that not much has changed here. The Framework
classes are essentially the same, and support for one or two new control surfaces has been added most
notably the APC20 and Serato controllers. Well leave the Serato scripts for a future investigation (they look
fascinating by the way), and take a peek under the hood of the APC20 scripts instead.

APC20 the APC40s little brother

The APC20 is basically an APC 40 minus the cross-fader, rotary controllers, and right hand buttons. To
maintain basic functionality with fewer buttons, some of the buttons on the left hand side are now dual-
purpose, as described in the APC20 Quickstart Guide. For example, the APC40s Stop All Clips button has
become the Shift button on the APC20. By holding down this button and selecting one of the Record buttons,
the APC20 sliders can be assigned to Volume, Pan, Send A , Send B, Send C, User 1, User 2, or User 3.

One new APC20 feature, which is not shared with the APC40, is Note Mode. The APC20 can be put into Note
Mode using the Note Mode button (which was the Master Select button on the APC40). This allows the 8x5
grid to be used for sending MIDI notes - to control a drum rack, for example. So, is there a Note Mode on the
APC40?
Sysex is used to set the APC modes, and while APC40 has 3 documented modes, the APC20 has 4. As shown in
the APC40 Communications Protocol document, a Type 0 sysex message is used to set the APC40s Modes.
Byte 8 is the Mode identifier:

0x40 (decimal 64) Generic Mode


0x41 (decimal 65) Ableton Live Mode
0x42 (decimal 66) Alternate Ableton Live Mode

The 8.1.3 MIDI remote scripts show that a new Mode byte value has been added for the APC20:

0x43 (decimal 67) APC20 Note Mode

Unfortunately, this new Mode value (0x43) is meaningless to the APC40 the hardware does not respond to
this value. This is not a remote script limitation, but rather a firmware update would be required in order to
for the APC40 to have a Note Mode. So far, none is available. On the other hand, it might well be possible to
create a script which emulates Note Mode by translating MIDI Note and Channel data on the fly (there is
already a Framework method for channel translation, which could be a good place to start). A future
scripting project, perhaps, if Akai decides not to provide firmware updates for the APC40.

Further investigation of the 8.1.3 scripts shows that the structure of the APC40 remote scripts has changed
somewhat, in order to accommodate the APC20. Both the APC20 and the APC40 classes are now sub-classes
of an APC super class. The APC class now handles the handshake, and other methods common to both
models. There is also a new APCSessionComponent module, which is used for linking multiple APCs (this
module appears to handle red box offsets, so that multiple session boxes for multiple APCs will sit side-by-
side). Here is the docstring for the class:

class APCSessionComponent(SessionComponent):
" Special SessionComponent for the APC controllers' combination mode "

Otherwise, little seems to have changed between 8.1.1 and 8.1.3. In fact, our APC40plus1 script runs
without error on 8.1.3, even though it was based on 8.1.1 code. Which is good news.

Now, just for fun, we can try to emulate an APC20 using an APC40, by overriding the sysex bytes for Product
ID and Mode. The product_model_id bytes for the APC20 would be 123 - for the APC40 its 115 (or hex 0x73,
as listed in the APC40 Communications Protocol document). So, we need to modify the APC20 script as
follows:

def _product_model_id_byte(self):
return 115 #was originally 123

And, in the APC20 consts.py file, we need to change the Note Mode value from 67 to 65:

MANUFACTURER_ID = 71
ABLETON_MODE = 65
NOTE_MODE = 65 #Was: 67 (= APC20 Note Mode); 65 = APC40 Ableton Mode 1

Well, the emulation does work, but of course, an APC40 is only half alive in APC20 mode the right hand
side is completely inactive and Note Mode does nothing (yet). On the other hand, the ability to use the
shift + record buttons to re-assign sliders is a nice feature. It would of course be possible to build a hybrid
script which combines features from both the APC20 and the APC40 scripts, if anyone felt so inclined. Simply
a question of finding the time - and we all know that there's never enough of that...

Conclusion

The MIDI remote scripts, and particularly the Framework classes, provide a convenient and relatively
straight-forward mechanism for tailoring the operation of control surfaces - with or without Max for Live.
The Live 8 scripts do cater to Max for Live, but maintain the same basic functionality theyve always had.
The APC40 scripts provide a good example of this, as do the new APC20 scripts - both of which provide a
good platform for customization.

Well, happy scripting - and remember to share your discoveries and custom scripts with the rest of the Live
community!

Hanz Petrov
April 13, 2010
Introduction to the Framework Classes Part 3
Introduction

As weve demonstrated previously, the _Framework classes provide a very useful framework for coding MIDI
control surface scripts in Python. Weve looked at basic script setup, worked with simple Control Surface
components and objects, and over-ridden high-level setup methods to achieve certain customization goals.
Here in this post, well have a look at the relationship between the _Framework classes and the special
subclass modules which are used with many of the Framework-based MIDI remote scripts for newer
controllers.

As a working example, well be adding APC20 functionality to the APC40 by modifying the APC40s MIDI
remote scripts. Our modifications will include Note Mode for the matrix, and User Mode for the sliders. Well
also have another look at Device Lock, create mappings for Undo and Redo, and re-map the APC40s endless
encoder to control Tempo. For those who wish to follow along, the complete Python source files which
accompany this article can be downloaded here.

Sysex and Subclasses

As weve seen previously, the APC20 can be put into Note Mode by sending an appropriate Sysex string to the
controller. In this mode, the 8x5 matrix can be used to send MIDI notes, trigger samples, control drum racks,
etc. - similar to the Launchpads User modes. For the APC40, however, a firmware upgrade would be
required in order enable Note Mode via Sysex. Fortunately, Sysex is not the only way. The Launchpads User
Modes do not rely on Sysex, and we can use the same approach to add a Note Mode to the APC40. Before we
do, however, lets collect the scripts we need in order to combine the useful new features of the APC20 with
those of the APC40.

In Live 8.1.3, the APC20 and APC40 scripts are both subclasses of the APC super-class script, although the
APC module actually resides in the APC40 folder, while the APC20 scripts are found in a separate folder.
Here, well copy all of the required scripts over into a new folder, which well call APC40_20. Our modified
controller script will then be selectable in Lives MIDI preferences drop-down list as APC40_20.

Since any shifted controls generally require a new subclass (as they inherit from and override Framework
base class methods), and since many of the controls on both the APC40 and on the APC20 work in a shifted
mode, well need to include a fair number of special subclass modules. Here is a list of the modules well
be needing (in addition to the Framework modules, which are not listed):

__init__.py
APC.py (APC40 and APC20)
APC40plus20.py (based on APC40 script)
APCSessionComponent.py (APC20)
ConfigurableButtonElement.py (Launchpad)
DetailViewControllerComponent.py (APC40)
EncoderMixerModeSelectorComponent.py (APC40)
PedaledSessionComponent.py (APC40)
RingedEncoderElement.py (APC40)
ShiftableDeviceComponent.py (APC40)
ShiftableSelectorComponent.py (APC20)
ShiftableTranslatorComponent.py (APC40)
ShiftableTransportComponent.py (APC40)
ShiftableZoomingComponent.py (APC20)
SliderModesComponent.py (APC20)
SpecialChannelStripComponent.py (APC40)
SpecialMixerComponent.py (APC40)
SpecialTransportComponent.py (APC40)

Note that weve included one non-APC module here, ConfigurableButtonElement, which is from the
Launchpad scripts. One advantage of using the Framework classes for scripting is that special subclass
modules are generally re-usable and interchangeable, especially for similar control surfaces.
Now, with all of the subclass scripts gathered together in one place (for ease of editing and transportability),
well merge the APC20 and APC40 scripts. Both of these scripts are Framework-based, so there will be some
import duplicates, which we can eliminate. There are also some APC20 special subclass modules which we
will be using instead of the APC40s plain vanilla Framework imports. Here is the merged import list:

import Live
from APC import APC
from _Framework.ControlSurface import ControlSurface
from _Framework.InputControlElement import *
from _Framework.SliderElement import SliderElement
from _Framework.ButtonElement import ButtonElement
from _Framework.EncoderElement import EncoderElement
from _Framework.ButtonMatrixElement import ButtonMatrixElement
from _Framework.MixerComponent import MixerComponent
from _Framework.ClipSlotComponent import ClipSlotComponent
from _Framework.ChannelStripComponent import ChannelStripComponent
from _Framework.SceneComponent import SceneComponent
#from _Framework.SessionZoomingComponent import SessionZoomingComponent #use
ShiftableZoomingComponent from APC20 scripts instead
from _Framework.ChannelTranslationSelector import ChannelTranslationSelector
from EncoderMixerModeSelectorComponent import
EncoderMixerModeSelectorComponent
from RingedEncoderElement import RingedEncoderElement
from DetailViewControllerComponent import DetailViewControllerComponent
from ShiftableDeviceComponent import ShiftableDeviceComponent
from ShiftableTransportComponent import ShiftableTransportComponent
from ShiftableTranslatorComponent import ShiftableTranslatorComponent
from PedaledSessionComponent import PedaledSessionComponent
from SpecialMixerComponent import SpecialMixerComponent

# Additional imports from APC20.py:


from ShiftableZoomingComponent import ShiftableZoomingComponent
from ShiftableSelectorComponent import ShiftableSelectorComponent
from SliderModesComponent import SliderModesComponent

# Import added from Launchpad scripts - needed for Note Mode:


from ConfigurableButtonElement import ConfigurableButtonElement

Next we compare the APC20 and APC40 scripts for differences, retaining the most useful parts of each. Since
most of the setting-up in the APC20 script setup is identical to that of APC40, and since the APC40 has more
controls than the APC20, well base our modified script on APC40 script, adding relevant code from the
APC20 script as needed. The basic set-up of our script includes the following components:

SessionComponent (includes session matrix, red box navigation controls, scene and clip launch buttons,
and stop buttons)
SessionZoomingComponent (includes zoomed out controls: matrix, selection, navigation, etc., for banks
of scenes)
MixerComponent (includes solo, mute, arm, and volume controls for tracks)
TransportComponent (includes play, stop, record, tempo, metronome, quantize, etc., for song)
DeviceComponent (includes device control, device on-off, device navigation, etc.)

These are basic Framework components, however, with a complex controller like the APC40, most are
actually instantiated as special subclass objects. The special subclass modules are typically used for over-
riding existing Framework class methods, and for adding new methods - and often include direct calls to the
LiveAPI. However, since they generally provide easily understood features, their usage is straight-forward
and we can include most of them as-is, without modification, and without further investigation. We do need
to sort out the conflicts between the APC40 and APC20 scripts, however - and add new code where we want
to do new things or do things in different ways. It turns out that the only special modules which well need
to modify here are the following:

SpecialMixerComponent.py we will map Cue Level to Tempo here


ShiftableDeviceComponent.py we will check the Device Lock state here
ShiftableSelectorComponent.py we will implement Note Mode here
ShiftableTransportComponent.py we will add Undo and Redo, and handle the true endless encoder here
ShiftableZoomingComponent.py we will modify the scope of Note Mode here (keep scene launch active)

Mapping Cue Level to Tempo provides a good example of subclass modification. It turns out that there is only
one true endless encoder on the APC40/20, which is the Cue Level control. This is the control which we want
to use as a Tempo control, due to its unique placement on the board. Unfortunately, however, the
simple set_tempo_control Framework TransportComponent method, which we would normally use, throws
an assertion error when mapped to a true endless encoder:

def set_tempo_control(self, control, fine_control = None):


assert ((control == None) or (isinstance(control, EncoderElement) and
(control.message_map_mode() is Live.MidiMap.MapMode.absolute)))
assert ((fine_control == None) or (isinstance(fine_control,
EncoderElement) and (fine_control.message_map_mode() is
Live.MidiMap.MapMode.absolute)))

Our true endless encoder is an EncoderElement with a map_mode value


of Live.MidiMap.MapMode.relative_two_compliment, not Live.MidiMap.MapMode.absolute, so, well need
to dig deeper. Luckily, theres an old-fashioned script which shows us the way - we can adapt code from
the Mackie Control script to suit our purpose here (the Mackie jog wheel is also a true endless encoder). In
the ShiftableTransportComponent module, we add a method for assigning the control, which now will throw
an assertion error if the mapped control is not an endless encoder:

def set_tempo_encoder(self, control):


assert ((control == None) or (isinstance(control, EncoderElement) and
(control.message_map_mode() is
Live.MidiMap.MapMode.relative_two_compliment)))
if (self._tempo_encoder_control != None):

self._tempo_encoder_control.remove_value_listener(self._tempo_encoder_value)
self._tempo_encoder_control = control
if (self._tempo_encoder_control != None):

self._tempo_encoder_control.add_value_listener(self._tempo_encoder_value)
self.update()

And we need to add the callback (i.e., the listener value). This is the method which is called whenever
the controller (encoder) is adjusted:

def _tempo_encoder_value(self, value):


if not self._shift_pressed:
assert (self._tempo_encoder_control != None)
assert (value in range(128))
backwards = (value >= 64)
step = 0.1 #step = 1.0 #reduce this for finer control; 1.0 is 1
bpm
if backwards:
amount = (value - 128)
else:
amount = value
tempo = max(20, min(999, (self.song().tempo + (amount * step))))
self.song().tempo = tempo

Note that weve changed the step increment to a reasonably smooth value of 0.1 bpm, since were only
using one encoder for tempo control here (the Framework basic transport.set_tempo_control method, which
were not using, is actually set up to accept two controller assignments - one for coarse tuning and one for
fine tempo tuning). If we wanted to, we could also adjust the Tempo range min and max values here as
well.

We can see that the _tempo_encoder_value method ends with a call to the LiveAPI; self.song().tempo =
tempo. The LiveAPI is well documented, so these types of calls are easy to look up, when they're not easliy
understood at first glance. We can also refer to the many MIDI remote scripts, and the _Framework scripts
themselves, for examples of how to make the calls and what arguments to use.

In the _tempo_encoder_value method above, we check to see if shift is pressed before doing anything else,
because we want to revert to the original Cue Volume mapping when in shifted mode. Well also need to add
some code to the SpecialMixerComponent module to do this. The _shift_value method is called when the
shift button is pressed, which in turn calls the update() method; this is where well connect our prehear
(Cue Volume) control - right before the Crossfader control connection is made:

def _shift_value(self, value): #added


assert (self._shift_button != None)
assert (value in range(128))
self._shift_pressed = (value != 0)
self.update()

def update(self): #added override


if self._allow_updates:
master_track = self.song().master_track
if self.is_enabled():
if (self._prehear_volume_control != None):
if self._shift_pressed: #added

self._prehear_volume_control.connect_to(master_track.mixer_device.cue_volume)
else:
self._prehear_volume_control.release_parameter()
#added
if (self._crossfader_control != None):

self._crossfader_control.connect_to(master_track.mixer_device.crossfader)
else:
if (self._prehear_volume_control != None):
self._prehear_volume_control.release_parameter()
if (self._crossfader_control != None):
self._crossfader_control.release_parameter()
if (self._bank_up_button != None):
self._bank_up_button.turn_off()
if (self._bank_down_button != None):
self._bank_down_button.turn_off()
if (self._next_track_button != None):
self._next_track_button.turn_off()
if (self._prev_track_button != None):
self._prev_track_button.turn_off()
self._rebuild_callback()
else:
self._update_requests += 1

So far, so good. With some higher-level remote script adjustments and some lower-level subclass code
modifications, weve successfully mapped Cue Level to Tempo. Now, on to Note Mode.

Note Mode

In order to enable Note Mode, first we need to disable the clip slots, so that we can re-assign the matrix
buttons to the MIDI notes of our chosing. We dont want to totally disconnect the ButtonElements, we only
want to change the mappings. The original APC20 script actually disables the SessionComponent altogether,
however, this also puts the Scene Launch and Track Stop buttons out of action. To take out only the matrix,
we need to disable the clip slots, and nothing else. Well do this in the set_ignore_buttons method of
theShiftableZoomingComponent class:

def set_ignore_buttons(self, ignore):


assert isinstance(ignore, type(False))
if (self._ignore_buttons != ignore): #if ignore state changes..
self._ignore_buttons = ignore #set new state
if (not self._is_zoomed_out): #if in session/clip view..
if ignore: #disable clip slots on ignore
for scene_index in range(5):
scene = self._session.scene(scene_index)
for track_index in range(8):
clip_slot = scene.clip_slot(track_index)
clip_slot.set_enabled(False)
else: #re-enable clip slots on ignore
for scene_index in range(5):
scene = self._session.scene(scene_index)
for track_index in range(8):
clip_slot = scene.clip_slot(track_index)
clip_slot.set_enabled(True)
#self._session.set_enabled((not ignore))

self.update()

Now that the clip slots are disabled, we can re-map the matrix buttons to MIDI notes. Well do this in
the _on_note_mode_changed method of the ShiftableSelectorComponent class. Well use a note layout
which is similar to the APC20s Note Mode layout, and to the Launchpads User 1 mode layout both of
which are based on split rows. Each half row will ascend in sets of 4 notes, which works well with the typical
Live Rack (also 4 notes wide). Here's a map of our new Note Mode layout (together with the other mods in
our script):
In order to avoid conflict with the APC40 assignments, well send all of our Note Mode notes out on channel
10 (the APC40 uses channels 1 through 8 - and possibly 9, according to the original note map). To map the
notes and to change the channels, well use the Framework set_channel and set_identifier methods. Well
also use the Framework button.send_value method to create different colour patterns for the left and right
sides of the grid so that our note mapping is visually obvious. Heres the code:

def _on_note_mode_changed(self):
if not self._master_button != None:
raise AssertionError
if self.is_enabled() and self._invert_assignment ==
self._toggle_pressed:
if self._note_mode_active:
self._master_button.turn_on()
for scene_index in range(5):
#TODO: re-map scene_launch buttons to note velocity...
scene = self._session.scene(scene_index)
for track_index in range(8):
clip_slot = scene.clip_slot(track_index)
button = self._matrix.get_button(track_index,
scene_index)
clip_slot.set_launch_button(None)
button.set_enabled(False)
button.set_channel(9) #remap all Note Mode notes to
channel 10
if track_index < 4:
button.set_identifier(52 - (4 * scene_index) +
track_index) #top row of left group (first 4 columns) notes 52 to 55
if (track_index % 2 == 0 and scene_index % 2 !=
0) or (track_index % 2 != 0 and scene_index % 2 == 0):
button.send_value(1) #0=off, 1=green, 2=green
blink, 3=red, 4=red blink, 5=yellow, 6=yellow blink, 7-127=green
else:
button.send_value(5)
else:
button.set_identifier(72 - (4 * scene_index) +
(track_index -4)) #top row of right group (next 4 columns) notes 72 to 75
if (track_index % 2 == 0 and scene_index % 2 !=
0) or (track_index % 2 != 0 and scene_index % 2 == 0):
button.send_value(1) #0=off, 1=green, 2=green
blink, 3=red, 4=red blink, 5=yellow, 6=yellow blink, 7-127=green
else:
button.send_value(3)
self._rebuild_callback()
else:
self._master_button.turn_off()
return None

Of course, we need to turn the lights off and re-enable the clip slots when Note Mode is switched back off.
Well do that in the _master_value method (which is also where the APC20 Sysex Note Mode string normally
gets queued-up):

def _master_value(self, value): #this is the master_button value_listener,


i.e. called when the master_button is pressed
if not self._master_button != None:
raise AssertionError
if not value in range(128):
raise AssertionError
if self.is_enabled() and self._invert_assignment ==
self._toggle_pressed:
if not self._master_button.is_momentary() or value > 0: #if the
master button is pressed:
#for button in self._select_buttons: #turn off track select
buttons (only needed for APC20)
#button.turn_off()
self._matrix.reset() #turn off the clip launch grid LEDs
#mode_byte = NOTE_MODE #= 67 for APC20 Note Mode, send as
part of sysex string to enable Note Mode
if self._note_mode_active: #if note mode is already on, turn
it off:
#mode_byte = ABLETON_MODE #= 65 for APC40 Ableton Mode 1
for scene_index in range(5):
scene = self._session.scene(scene_index)
for track_index in range(8):
clip_slot = scene.clip_slot(track_index)
button = self._matrix.get_button(track_index,
scene_index)
clip_slot.set_launch_button(button)
button.set_enabled(True)
button.turn_off()
self._rebuild_callback()
#self._mode_callback(mode_byte) #send sysex to set Mode
(NOTE_MODE or ABLETON_MODE)
self._note_mode_active = not self._note_mode_active
self._zooming.set_ignore_buttons(self._note_mode_active)
#turn off matrix, scene launch, and clip stop buttons when in Note Mode
#self._transport.update() #only needed for APC20
self._on_note_mode_changed()
return None

So now we have a working Note Mode on the APC40 (dont forget that we do have to enable the MIDI Input
Track switch in Live's MIDI preferences dialog, so that our notes get passed along to the track). Not too
difficult, was it? Of course, I wouldnt expect to see official support for Note Mode on the APC40 from
Ableton or Akai anytime soon, if only because thousands of units are already out there, without any red
stenciling to tell users which button to press to enable Note Mode and they might get confused. Well, that,
and it might hurt sales of the APC20.

Undo/Redo

Now, before we sign off, well take a minute and re-map the Tempo Nudge buttons to Undo and Redo. It just
so happens that the OpenLabs scripts have a nice SpecialTransportComponent module, which provides
methods for setting Undo, Redo, and Back to Start (BTS) buttons; well copy the required code over to our
equivalent ShiftableTransportComponent module. Here is the Undo code (Redo is almost identical):

def set_undo_button(self, undo_button):


assert isinstance(undo_button, (ButtonElement,
type(None)))
if (undo_button != self._undo_button):
if (self._undo_button != None):
self._undo_button.remove_value_listener(self._undo_value)
self._undo_button = undo_button
if (self._undo_button != None):
self._undo_button.add_value_listener(self._undo_value)
self.update()

And the callback (featuring some more LiveAPI calls):

def _undo_value(self, value):


assert (self._undo_button != None)
assert (value in range(128))
if self.is_enabled():
if ((value != 0) or (not self._undo_button.is_momentary())):
if self.song().can_undo:
self.song().undo()

Conclusion

Well, thats it for now. Weve shown that implementing Note Mode on the APC40 is relatively straight-
forward, and does not in fact require a firmware update. Weve also demonstrated that much can be
accomplished simply by extending Framework-based scripts and subclasses. Our custom tweaks and mappings
do not require templates or add-ons, and best part is that they are free free as in free beer! Cheers.

Hanz Petrov
May 13, 2010

hanz.petrov
at gmail.com

PostScript: It would seem that many (if not all) of the Live MIDI remote scripts, for all of the various control
surfaces, were coded by Jan B. over at Ableton. His code is a pleasure to work with and fun to learn from -
my appreciation goes out to Jan and to the entire Ableton team.
Introduction to the Framework Classes Part 4 - the Final Chapter
Introduction

In Part 1 of this series, I mentioned that my journey into the world of MIDI remote scripts began with a
search for a better way of integrating my FCB1010 foot controller into my Live setup. Well, its been a fun
trip - with lots of interesting side investigations along the way - but now weve come full circle and its time
to finish up what I set out to do in the beginning. In this article, well have a look a couple of new
_Framework methods introduced in version 8.1.3, which will allow us to operate scripts in Combination
Mode, and well create a generic script which will allow the FCB1010 to work in concert with the APC40 in
fact, well set it up to emulate the APC40. And then were pretty much done. Lets start with combination
mode.

Combination Mode red and yellow and pink and green

As we saw in Part 1, set_show_highlight, a SessionComponent method, can be used to display the famous
red box, which represents the portion of a Session View which a control surface is controlling. First seen
with the APC40 and Launchpad, the red box is a must have for any clip launcher. New since 8.1.4, however,
is a functional Combination Mode specifically announced for the APC40 and APC20 which glues two or
more session highlights together. From the 8.1.4 changelog:

Combination Mode is now active when multiple Akai APC40/20s are in use. This means that the topmost
controller selected in your preferences will control tracks 1-8, the second controller selected will control
tracks 9-16, and so on.

Sounds like fun lets see how its done.

By looking through the APC sources, we can work our way back to the essential change at work here: the
SessionComponent class now has new methods called _link and _unlink (together with a new
attribute _is_linked, and a new list object known as _linked_session_instances). Linking sessions turns out to
be no more difficult than calling the SessionComponents _link method, as follows:

session = SessionComponent(num_tracks, num_scenes)


session._link()

Now, although multiple session boxes can be linked in this way, we need to manage our session offsets, if we
want our sessions to sit beside each other, and not one on top of the other. In other words, we will need to
sequentially assign non-zero offsets to our linked sessions, based on the adjacent sessions width. We'll add
the required code to the ProjectX script from Part 1, as an illustration.

First, well need a list object to hold the active instances of our ProjectX ControlSurface class, and a static
method which will be called at the end of initialisation:

_active_instances = []

def _combine_active_instances():
track_offset = 0
for instance in ProjectX._active_instances:
instance._activate_combination_mode(track_offset)
track_offset += session.width()

_combine_active_instances = staticmethod(_combine_active_instances)

We add a _do_combine call at the end of our init sequence, which in turn calls
the _combine_active_instances static method
and _combine_active_instances calls _activate_combination_mode on each instance.
def _do_combine(self):
if self not in ProjectX._active_instances:
ProjectX._active_instances.append(self)
ProjectX._combine_active_instances()

def _activate_combination_mode(self, track_offset):


if session._is_linked():
session._unlink()
session.set_offsets(track_offset, 0)
session._link()

Now that the linking is all set up, well define a _do_uncombine method, to clean things up when we
disconnect; well unlink our SessionComponent and remove ourselves from the list of active instances here.

def _do_uncombine(self):
if ((self in ProjectX._active_instances) and
ProjectX._active_instances.remove(self)):
self._session.unlink()
ProjectX._combine_active_instances()

def disconnect(self):
self._do_uncombine()
ControlSurface.disconnect(self)

So heres what we get when we load several instances of our new ProjectX script via Live's MIDI preferences
dialog:

The session highlights are all linked (with an automatic track offset for each instance, which matches the
adjacent session highlight width), and when we move any one of them, the others move along together. By
changing the offsets in _activate_combination_mode, we can get them to stack side-by-side, or one above
the other, or if we wanted to, we could indeed stack them one on top of the other. By stacking them one on
top of the other, we can control a single session highlight zone with multiple controllers which is exactly
what we want to do with the APC40 and FCB1010 in combination mode.

As of version 8.1.3, the Framework scripts have included support for session linking, but so far, the only
official scripts which implement combination mode are the APC scripts (as of 8.1.4). As shown above,
however, support for combination mode can be extended to pretty much any Framework-based script. What
we want to create then, is a script which will allow the FCB1010 to operate in combination mode together
with the APC40. Lets build one.
FCB1020 - a new script for the FCB1010

The first thing to do when setting out to create a new script is to decide on a functional layout. If we know
what were trying to achieve in terms of functionality and operation - before we touch a line of code - we
can save ourselves a good deal of time down the road. In this case, were looking for an arrangement which
will allow the FCB1010 to mirror the operation of the APC40, in so far as possible, and allow for optimized
hands-free operation.

Although the options for designing a control script are almost unlimited, generally, the resulting method of
operation needs to be intuitive. The FCB1010 has some built-in constraints, but also offers a great deal of
flexibility. We have 10 banks of 10 switches and 2 pedals to work with equivalent to 100 button controls
and 20 sliders. Interestingly, the APC40 has a similar number of buttons and knobs.

If we look at the two controllers side-by-side, a pattern emerges. Each column of the APC40s grid consists
of 5 clip launch buttons (one per scene) and 5 track control buttons (clip stop, track select, activate, solo,
& record). Each of the FCB1010s 10 banks has 5 switches on the top row, and 5 switches on the bottom row.
Based on this parallel, if we assign one FCB1010 bank to each of the APC40s track columns, the resulting
operation will indeed be intuitive, and will closely follow the APCs layout. We only have 2 pedals per bank,
however, so well map them to Track Volume, and Send A at least for now. Heres how a typical FCB1010
bank will lay out, together with the APC40 layout for comparison.

Bank 01

APC40

Well use this layout for banks 1 through 8 (since the APC40 has 8 track control columns), but because the
FCB1010 has 10 banks in total, we have 2 banks left over. Lets use bank 00 for the Master Track controls,
and the scene launch controls, in a similar arrangement to banks 1 through 8. There are no activate, solo or
record buttons for the master track, so instead, well map global play, stop and record here:

Bank 00

Now we only have one bank left - bank 09. Well use bank 09 for session and track navigation, and for device
control. Heres how it will look:

Bank 09

Although fairly intuitive, the layout described above might not suit everyones preferences. Wouldnt it be
nice if the end user could decide on his or her own preferred layout? Lives User Remote Scripts allow for
this kind of thing, so well take a similar approach. Rather than hard-coding the note and controller
mappings deep within our new script, well pull all of the assignments out into a separate file, for easy
access and editing. It will remain a python .py file (not a .txt file) - in the tradition of consts.py, of Mackie
emulation fame - but since .py files are simple text files, they can be edited using any text editor. Well call
our file MIDI_map.py. Heres a sample of what it will contain:

# General
PLAY = 7 #Global play
STOP = 8 #Global stop
REC = 9 #Global record
TAPTEMPO = -1 #Tap tempo
NUDGEUP = -1 #Tempo Nudge Up
NUDGEDOWN = -1 #Tempo Nudge Down
UNDO = -1 #Undo
REDO = -1 #Redo
LOOP = -1 #Loop on/off
PUNCHIN = -1 #Punch in
PUNCHOUT = -1 #Punch out
OVERDUB = -1 #Overdub on/off
METRONOME = -1 #Metronome on/off
RECQUANT = -1 #Record quantization on/off
DETAILVIEW = -1 #Detail view switch
CLIPTRACKVIEW = -1 #Clip/Track view switch
# Device Control
DEVICELOCK = 99 #Device Lock (lock "blue hand")
DEVICEONOFF = 94 #Device on/off
DEVICENAVLEFT = 92 #Device nav left
DEVICENAVRIGHT = 93 #Device nav right
DEVICEBANKNAVLEFT = -1 #Device bank nav left
DEVICEBANKNAVRIGHT = -1 #Device bank nav right

# Arrangement View Controls


SEEKFWD = -1 #Seek forward
SEEKRWD = -1 #Seek rewind

# Session Navigation (aka "red box")


SESSIONLEFT = 95 #Session left
SESSIONRIGHT = 96 #Session right
SESSIONUP = -1 #Session up
SESSIONDOWN = -1 #Session down
ZOOMUP = 97 #Session Zoom up
ZOOMDOWN = 98 #Session Zoom down
ZOOMLEFT = -1 #Session Zoom left
ZOOMRIGHT = -1 #Session Zoom right

# Track Navigation
TRACKLEFT = 90 #Track left
TRACKRIGHT = 91 #Track right

# Scene Navigation
SCENEUP = -1 #Scene down
SCENEDN = -1 #Scene up

# Scene Launch
SELSCENELAUNCH = -1 #Selected scene launch

Now we can easily change the layout of any of our banks, by editing this one file. In fact, pretty much
anything goes if we wanted to, we could have different layouts for each of the 10 banks, or leave some
banks unassigned, for use with guitar effects, etc. To help with layout planning, an editable PDF template
for the FCB1010 is included with the source code on the Support Files page.

Okay, so now its time to assemble the code. Since were essentially emulating the APC40 here (yes, I admit
that I was wrong in Part 2; APC40 emulation is not so crazy after all), we have a choice between starting
with the APC scripts and customizing, or building a new set of scripts which have similar functionality. Since
we wont be supporting shifted operations in our script (for the FCB1010 this would require operation with
two feet difficult to do in an upright position), we will need to make significant changes to the APC scripts.
Starting from scratch is definitely an option worth considering. On the other hand, the APC40 script will
make for a good roadmap, and while were at it, we can include some of the special features of the
APC40_22 script from Part 3 here as well.

The structure will be fairly simple. Well have an __init__.py module (to identify the directory as a python
package), a main module (called FCB1020.py), a MIDI_map.py constants file, and several special
Framework component override modules. Heres the file list (compete source code is available on the
Support Files page):

__init__.py
FCB1020.py
MIDI_Map.py
SpecialChannelStripComponent.py
SpecialMixerComponent.py
SpecialSessionComponent.py
SpecialTransportComponent.py
SpecialViewControllerComponent.py
SpecialZoomingComponent.py

We wont go into detail on the Special components, since that topic was covered in Part 3. The main module
follows the structure outlined in Part 1, but here's a quick overview. We start with the imports, and then
define the combination mode static method (as discussed above):

import Live
from _Framework.ControlSurface import ControlSurface
from _Framework.InputControlElement import *
from _Framework.SliderElement import SliderElement
from _Framework.ButtonElement import ButtonElement
from _Framework.ButtonMatrixElement import ButtonMatrixElement
from _Framework.ChannelStripComponent import ChannelStripComponent
from _Framework.DeviceComponent import DeviceComponent
from _Framework.ControlSurfaceComponent import ControlSurfaceComponent
from _Framework.SessionZoomingComponent import SessionZoomingComponent
from SpecialMixerComponent import SpecialMixerComponent
from SpecialTransportComponent import SpecialTransportComponent
from SpecialSessionComponent import SpecialSessionComponent
from SpecialZoomingComponent import SpecialZoomingComponent
from SpecialViewControllerComponent import DetailViewControllerComponent
from MIDI_Map import *

class FCB1020(ControlSurface):
__doc__ = " Script for FCB1010 in APC emulation mode "
_active_instances = []
def _combine_active_instances():
track_offset = 0
scene_offset = 0
for instance in FCB1020._active_instances:
instance._activate_combination_mode(track_offset, scene_offset)
track_offset += instance._session.width()
_combine_active_instances = staticmethod(_combine_active_instances)

Next we have our init method, where we instantiate our ControlSurface component and call the various
setup methods. We setup the session, then setup the mixer, then assign the mixer to the session, to keep
them in sync. The disconnect method follows, where we provide some cleanup for when the control surface
is disconnected:

def __init__(self, c_instance):


ControlSurface.__init__(self, c_instance)
self.set_suppress_rebuild_requests(True)
self._note_map = []
self._ctrl_map = []
self._load_MIDI_map()
self._session = None
self._session_zoom = None
self._mixer = None
self._setup_session_control()
self._setup_mixer_control()
self._session.set_mixer(self._mixer)
self._setup_device_and_transport_control()
self.set_suppress_rebuild_requests(False)
self._pads = []
self._load_pad_translations()
self._do_combine()

def disconnect(self):
self._note_map = None
self._ctrl_map = None
self._pads = None
self._do_uncombine()
self._shift_button = None
self._session = None
self._session_zoom = None
self._mixer = None
ControlSurface.disconnect(self)

The balance of the combination mode methods are next:

def _do_combine(self):
if self not in FCB1020._active_instances:
FCB1020._active_instances.append(self)
FCB1020._combine_active_instances()

def _do_uncombine(self):
if ((self in FCB1020._active_instances) and
FCB1020._active_instances.remove(self)):
self._session.unlink()
FCB1020._combine_active_instances()

def _activate_combination_mode(self, track_offset, scene_offset):


if TRACK_OFFSET != -1:
track_offset = TRACK_OFFSET
if SCENE_OFFSET != -1:
scene_offset = SCENE_OFFSET
self._session.link_with_track_offset(track_offset, scene_offset)

The session setup is based on Framework SessionComponent methods, with SessionZoomingComponent


navigation thrown in for good measure:

def _setup_session_control(self):
is_momentary = True
self._session = SpecialSessionComponent(8, 5)
self._session.name = 'Session_Control'
self._session.set_track_bank_buttons(self._note_map[SESSIONRIGHT],
self._note_map[SESSIONLEFT])
self._session.set_scene_bank_buttons(self._note_map[SESSIONDOWN],
self._note_map[SESSIONUP])
self._session.set_select_buttons(self._note_map[SCENEDN],
self._note_map[SCENEUP])
self._scene_launch_buttons = [self._note_map[SCENELAUNCH[index]] for
index in range(5) ]
self._track_stop_buttons = [self._note_map[TRACKSTOP[index]] for
index in range(8) ]
self._session.set_stop_all_clips_button(self._note_map[STOPALLCLIPS])

self._session.set_stop_track_clip_buttons(tuple(self._track_stop_buttons))
self._session.set_stop_track_clip_value(2)
self._session.selected_scene().name = 'Selected_Scene'

self._session.selected_scene().set_launch_button(self._note_map[SELSCENELAUNC
H])
self._session.set_slot_launch_button(self._note_map[SELCLIPLAUNCH])
for scene_index in range(5):
scene = self._session.scene(scene_index)
scene.name = 'Scene_' + str(scene_index)
button_row = []
scene.set_launch_button(self._scene_launch_buttons[scene_index])
scene.set_triggered_value(2)
for track_index in range(8):
button =
self._note_map[CLIPNOTEMAP[scene_index][track_index]]
button_row.append(button)
clip_slot = scene.clip_slot(track_index)
clip_slot.name = str(track_index) + '_Clip_Slot_' +
str(scene_index)
clip_slot.set_launch_button(button)
self._session_zoom = SpecialZoomingComponent(self._session)
self._session_zoom.name = 'Session_Overview'
self._session_zoom.set_nav_buttons(self._note_map[ZOOMUP],
self._note_map[ZOOMDOWN], self._note_map[ZOOMLEFT],
self._note_map[ZOOMRIGHT])

Mixer, device, and transport setup methods are similar.

def _setup_mixer_control(self):
is_momentary = True
self._mixer = SpecialMixerComponent(8)
self._mixer.name = 'Mixer'
self._mixer.master_strip().name = 'Master_Channel_Strip'

self._mixer.master_strip().set_select_button(self._note_map[MASTERSEL])
self._mixer.selected_strip().name = 'Selected_Channel_Strip'
self._mixer.set_select_buttons(self._note_map[TRACKRIGHT],
self._note_map[TRACKLEFT])
self._mixer.set_crossfader_control(self._ctrl_map[CROSSFADER])
self._mixer.set_prehear_volume_control(self._ctrl_map[CUELEVEL])

self._mixer.master_strip().set_volume_control(self._ctrl_map[MASTERVOLUME])
for track in range(8):
strip = self._mixer.channel_strip(track)
strip.name = 'Channel_Strip_' + str(track)
strip.set_arm_button(self._note_map[TRACKREC[track]])
strip.set_solo_button(self._note_map[TRACKSOLO[track]])
strip.set_mute_button(self._note_map[TRACKMUTE[track]])
strip.set_select_button(self._note_map[TRACKSEL[track]])
strip.set_volume_control(self._ctrl_map[TRACKVOL[track]])
strip.set_pan_control(self._ctrl_map[TRACKPAN[track]])
strip.set_send_controls((self._ctrl_map[TRACKSENDA[track]],
self._ctrl_map[TRACKSENDB[track]], self._ctrl_map[TRACKSENDC[track]]))
strip.set_invert_mute_feedback(True)

def _setup_device_and_transport_control(self):
is_momentary = True
self._device = DeviceComponent()
self._device.name = 'Device_Component'
device_bank_buttons = []
device_param_controls = []
for index in range(8):
device_param_controls.append(self._ctrl_map[PARAMCONTROL[index]])
device_bank_buttons.append(self._note_map[DEVICEBANK[index]])
if None not in device_bank_buttons:
self._device.set_bank_buttons(tuple(device_bank_buttons))
self._device.set_parameter_controls(tuple(device_param_controls))
self._device.set_on_off_button(self._note_map[DEVICEONOFF])
self._device.set_bank_nav_buttons(self._note_map[DEVICEBANKNAVLEFT],
self._note_map[DEVICEBANKNAVRIGHT])
self._device.set_lock_button(self._note_map[DEVICELOCK])
self.set_device_component(self._device)

detail_view_toggler = DetailViewControllerComponent()
detail_view_toggler.name = 'Detail_View_Control'

detail_view_toggler.set_device_clip_toggle_button(self._note_map[CLIPTRACKVIE
W])

detail_view_toggler.set_detail_toggle_button(self._note_map[DETAILVIEW])

detail_view_toggler.set_device_nav_buttons(self._note_map[DEVICENAVLEFT],
self._note_map[DEVICENAVRIGHT] )

transport = SpecialTransportComponent()
transport.name = 'Transport'
transport.set_play_button(self._note_map[PLAY])
transport.set_stop_button(self._note_map[STOP])
transport.set_record_button(self._note_map[REC])
transport.set_nudge_buttons(self._note_map[NUDGEUP],
self._note_map[NUDGEDOWN])
transport.set_undo_button(self._note_map[UNDO])
transport.set_redo_button(self._note_map[REDO])
transport.set_tap_tempo_button(self._note_map[TAPTEMPO])
transport.set_quant_toggle_button(self._note_map[RECQUANT])
transport.set_overdub_button(self._note_map[OVERDUB])
transport.set_metronome_button(self._note_map[METRONOME])
transport.set_tempo_control(self._ctrl_map[TEMPOCONTROL])
transport.set_loop_button(self._note_map[LOOP])
transport.set_seek_buttons(self._note_map[SEEKFWD],
self._note_map[SEEKRWD])
transport.set_punch_buttons(self._note_map[PUNCHIN],
self._note_map[PUNCHOUT])

Weve also included a DetailViewComponent above, which communicates session view changes via the Live
API. Next is _on_selected_track_changed, a ControlSurface class method override, which keeps the selected
tracks device in focus. And for drum rack note mapping, weve included a _load_pad_translationsmethod,
which adds x and y offsets to the Drum Rack note and channel assignments, which are set in
the MIDI_map.py file. This allows us to pass the translations array as an argument to the
ControlSurface set_pad_translations method in the expected format.

def _on_selected_track_changed(self):
ControlSurface._on_selected_track_changed(self)
track = self.song().view.selected_track
device_to_select = track.view.selected_device
if device_to_select == None and len(track.devices) > 0:
device_to_select = track.devices[0]
if device_to_select != None:
self.song().view.select_device(device_to_select)
self._device_component.set_device(device_to_select)

def _load_pad_translations(self):
if -1 not in DRUM_PADS:
pad = []
for row in range(4):
for col in range(4):
pad = (col, row, DRUM_PADS[row*4 + col], PADCHANNEL,)
self._pads.append(pad)
self.set_pad_translations(tuple(self._pads))

Finally, we have _load_MIDI_map. Here, we create a list of ButtonElements and a list of SliderElements.
When we make mapping assignments in our MIDI_map.py file, we are actually indexing objects from these
lists. By instantiating the ButtonElements and SliderElements as independent objects, we limit the risk of
duplicate MIDI assignments, which would prevent our script from loading. Any particular MIDI note/channel
message from a control surface can only be assigned to a single InputControlElement (such as a button or
slider), however, an InputControlElement can be used more than once, with different components. This
setup also allows us to append None to the end of each list, so that null assignments can be specified in the
MIDI_map file, by using -1 in place of a note number (in python, [-1] corresponds to the last element of a
list).

def _load_MIDI_map(self):
is_momentary = True
for note in range(128):
button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, NOTECHANNEL,
note)
button.name = 'Note_' + str(note)
self._note_map.append(button)
self._note_map.append(None) #add None to the end of the list,
selectable with [-1]
for ctrl in range(128):
control = SliderElement(MIDI_CC_TYPE, CTRLCHANNEL, ctrl)
control.name = 'Ctrl_' + str(ctrl)
self._ctrl_map.append(control)
self._ctrl_map.append(None)

Now, speaking of MIDI assignments, since all of our mappings are editable, and grouped in a separate file,
couldnt we use our script with just about any control surface, and not only the FCB1010? Yes, indeed we
could.

Generic APC Emulation

Our new FCB10120 script can be used as a generic APC emulator, since it merely maps MIDI Note and CC
input to specific _Framework component functions, mimicking the APC script setup. In fact, none of this is
very different from the User script mechanism provided by Ableton - although our script has a few extra
features that the User script does not support, including Session Highlighting (aka red box), and the ability
to work in combination mode, with an APC40, or with another instance of itself, or with any other controller
which supports combination mode.

Some setup is required, however, to accommodate alternate controller configurations. These configurations
could be alternate configurations for the same control surface, or alternate configurations for different
control surfaces.

Probably the simplest way of setting up an alternate configuration, is to create one or more copies of the
FCB1020 script folder, modify the assignments in the MIDI_map.py file as required, and then re-name the
directory to suit. The new folder will be selectable by name from Lives control surface drop-down list, the
next time Live is started. This way, one could have,
say, FCB1020_device_mode and FCB1020_transport_mode as separate configurations, listed one above the
other in the control surface drop-down. Note however, that one should avoid leading underscores in folder
name, unless a folder is intended to be hidden.

Another way to accommodate alternate setups would be to reprogram the control surface itself where this
is possible - to match the note and CC mappings found in the MIDI_map.py file. Depending on the control
surface, this could be done manually, with stand-alone software, or by using Lives dump button from the
preferences dialog (in fact, a sysex file for the FCB1010 is included with the support files package which
accompanies this article, for this purpose).

Of course, our script can also be used as a generic APC emulator for multiple controllers at the same time.
This can be done a number of ways, including:
1) Daisy-chain several control surfaces using MIDI Thru ports;
2) Load the script multiple times, using multiple slots;
3) Create multiple copies of the script folder, rename to suit, and load into different slots.

For multiple control surfaces which use different MIDI channels, separate instances of the script would need
to be loaded, from separate folders, with the channel assignments in the MIDI_maps.py modified to suit in
each folder.

And getting back to our original design setup, we can see that the FCB1010 and the APC40 now work well
together in Combination Mode, and the FCB1010 is able to control of most of the APCs functions within
one session highlight area, and without loss of focus. We have included a good deal of flexibility in our script
too, so we can easily modify the various bank layouts to suit our needs, as they develop and change.

Conclusion

In this new era of multi-touch controllers, its nice to know that a sturdy old workhorse like the FCB1010 still
has a place in our arsenal of control surfaces - and that it works well in combination with the soon-to-be-a-
classic and not-yet-obsolete APC40. As for MIDI remote scripts, theyre still at the heart of all control surface
communications with the Live API - and the _Framework classes have been holding their own for quite a
while now, with interesting new methods being added from time to time. Hopefully, this series of articles
has been useful, and will encourage others to share their findings with the Live community. Happy scripting.

Hanz Petrov
September 7, 2010

hanz.petrov
at gmail.com

Вам также может понравиться