Monday, April 12, 2010

Introduction to the Framework Classes Part 2

Background

In this post, we’ll 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 you’re 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 8’s 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 we’re 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 Live’s LiveAPI resource patches (M4L.api.SelectComponent, M4L.api.SelectControlSurface, M4L.api.SelectControl, etc.). Before we demonstrate, however, let’s have a look at how Control Surfaces fit into the world of Max’s 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, let’s 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. We’ll 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. There’s 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? It’s not hiding anything that can’t 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. Here’s 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 they’re someone who already owns an APC40?! I suspect that the majority of Monome owners probably wouldn’t be interested in using their high-end boutique hardware to emulate a mass-market controller like the APC40, and besides, they’ve 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, let’s have a quick look at how we can manipulate the APC40’s LEDs using a remote script. We’ll 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), we’d need to override this part of the script.

Now, based on this information, we’ll 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, we’ll 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, let’s create a new script which inherits from the APC40 class. We’ll call our new control surface script APC40plus1.

Here’s the new _init__.py code for our new script:

from APC40plus1 import APC40plus1
def create_instance(c_instance):
    return APC40plus1(c_instance)

And here’s 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” doesn’t show up, we know that something is wrong, and we can check the Live log file for errors, and trouble-shoot from there. We’ve 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, we’ll 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_loop message 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, let’s try to do something more useful here – we’ll re-assign the Metronome button to act as a Device Lock button. We’ll 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 isn’t 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, we’ll need to do some basic setup.

First, we instantiate an APC40 ControlSurface object, then we initialize using the APC40’s __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 we’ve made functional changes without modifying the original APC40 scripts, we’re still dependent on those original scripts, because we inherit methods from them. This means that we’ll need to watch out for MIDI remote script changes in new versions of Live, since they might break our new script. So why don’t 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. We’ll 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 APC40’s 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 APC40’s 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 APC40’s 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 it’s 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 they’ve 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