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, it’s been a fun trip - with lots of interesting side investigations along the way - but now we’ve come full circle and it’s time to finish up what I set out to do in the beginning. In this article, we’ll 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 we’ll create a generic script which will allow the FCB1010 to work in concert with the APC40 – in fact, we’ll set it up to emulate the APC40. And then we’re pretty much done. Let’s 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 APC 20– 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 – let’s see how it’s 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 SessionComponent’s _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 session’s width. We'll add the required code to the ProjectX script from Part 1, as an illustration.
First, we’ll 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, we’ll define a _do_uncombine method, to clean things up when we disconnect; we’ll 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 here’s 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 si ngle session highlight zone wi th 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 m ode together with the APC40. Let’s 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 we’re 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, we’re looking for an arrangement which will allow the FCB1010 to mirror the operation of the AP C40, 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 meth od 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 APC40’s 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 FCB1010’s 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 APC40’s track columns, the resulting operation will indeed be intuitive, and will closely follow the APC’s layout. We only have 2 pedals per bank, however, so we’ll map them to Track Volume, and Send A – at least for now. Here’s how a typical FCB1010 bank will lay out, together with the A PC40 layout for comparison.
Bank 01
APC40
We’ll 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. Let’s 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, we’ll map global play, stop and record here:
Bank 00
Now we only have one bank left - bank 09. We’ll use bank 09 for session and track navigation, and for device control. Here’s how it will look:
Bank 09
Although fairly intuitive, the layout described above might not suit everyone’s preferences. Wouldn’t it be nice if the end user could decide on his or her own preferred layout? Live’s User Remote Scripts allow for this kind of thing, so we’ll take a similar approach. Rather than hard-coding the note and controller mappings deep within our new script, we’ll pull all of the assignments out into a separate file, for easy access and editing. It will rem ain 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. We’ll call our file MIDI_map.py . Here’s 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 it’s time to assemble the code. Since we’re essentially emulating the APC40 here (yes, I admit that I was wrong in Part 2; APC40 emulation is not so crazy after all), we h ave a choice between starting with the APC scripts and customizing, or building a new set of scripts which have similar functionality. Since we won’t 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 si gnificant 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 we’re 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. We’ll 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. Here’s 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 won’t 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 f or 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])
We’ve 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 track’s device in focus. And for drum rack note mapping, we’ve 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 ControlSurfaceset_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 ofButtonElements 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 preve nt our script from loading. Any pa rticular 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, couldn’t 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