While designing my live audio setup for Psychedelic Friendship Bingo, I ran into a frustrating limitation. I wanted to launch clips in Ableton Live 9 using the button matrix on an APC40, and I wanted to automatically limit the matrix to playing one clip at a time. In other words, pressing a button on the matrix would send a ‘stop’ signal to all other clips in the matrix. This is not possible with Live and the APC out of the box.

Working with MIDI Remote Scripts

To achieve this, I had to dive into Live’s MIDI Remote Scripts: Python scripts installed with Live that perform all of the complex automatic mapping of commonly used controllers like the APC. In OS X, my scripts are located in:

/Applications/Ableton Live 9 Suite.app/Contents/App-Resources/MIDI Remote Scripts/

In the MIDI Remote Scripts/APC40/ subdirectory, you’ll see that the files are compiled into Python bytecode with the .pyc extension. It took me a while to find decompiled scripts for the APC40 that would recompile correctly for Live 9, but I eventually found great working scripts provided by Gangsterish Music.

The APC40/ directory they provide will match your existing MIDI Remote Scripts/APC40/ directory, but the files will have a .py extension, indicating that they’ve been decompiled into readable Python. Rename the new APC40/ directory to something like APC40_custom/, and drop it into MIDI Remote Scripts/. Now, whenever you start up Live, the .py files will be automatically recompiled and cached until you quit the application. So keep in mind that whenever you make changes to these scripts, you’ll need to restart Live for them to take effect.

Ableton’s _Framework

These MIDI Remote Scripts utilize Ableton’s MIDI Remote Scripts/_Framework/ package, which provides a high-level API for interacting with MIDI controllers. Julien Bayle maintains a GitHub repository with decompiled versions of the entire MIDI Remote Scripts/ directory, including the _Framework package. This was invaluable as a reference for this project, but the repository’s APC40 scripts themselves did not work for me when recompiled (so use the files from Gangsterish linked to above). Julien has also auto-generated documentation for the _Framework package.

The file APC40_custom/APC40.py defines APC40, a subclass of ControlSurface from the module _Framework.ControlSurface. Its initial setup is performed by the function _setup_session_control, which is a good starting point for figuring out what’s going on in these scripts. In the definition of that function, you’ll see the line:

matrix = ButtonMatrixElement()

which instantiates the class imported at the top:

from _Framework.ButtonMatrixElement import ButtonMatrixElement

This is the class we’ll be working with in order to customize the behavior of matrix buttons.

Customizing ButtonMatrixElement

By default, launching a clip in a particular track stops all other clips in that same track. Each track also has a dedicated ‘track stop’ button that will stop all of its clips. So an efficient way to achieve my desired behavior is: whenever a button is pushed on some track (column) in the matrix, I will programmatically press the ‘track stop’ button on every other matrix track.

The class ButtonMatrixElement (mentioned above) has a function on_nested_control_element_value that is called whenever a matrix button is pushed. This is the function that needs to be overridden to perform both its default behavior—launching a clip, thereby stopping all other clips in the same track—and our new behavior—activating all of the other ‘track stop’ buttons. In order to do this, we’ll create a subclass of ButtonMatrixElement called CustomButtonMatrixElement.

First, in APC40_custom/APC40.py, comment out the import statement quoted above and replace it with an import from the new module we’re about to create:

# from _Framework.ButtonMatrixElement import ButtonMatrixElement

from CustomButtonMatrixElement import CustomButtonMatrixElement

Then, change the instantiation quoted above to an instantiation of our new class. We will have to pass the constructor a reference to the APC40 instance itself—as control_surface—in order to have access to its ‘track stop’ buttons later:

# matrix = ButtonMatrixElement()

matrix = CustomButtonMatrixElement(control_surface=self)

Now, create the file APC40_custom/CustomButtonMatrixElement.py, and put the necessary imports at the top:

import Live

from _Framework.ButtonMatrixElement import ButtonMatrixElement

Define the new subclass and its __init__ function to make sure that its parent’s __init__ is called as well:

class CustomButtonMatrixElement(ButtonMatrixElement):

def __init__(self, control_surface, rows = [], *a, **k):

parent = super(CustomButtonMatrixElement, self)

parent.__init__(rows, *a, **k)

We’ll also assign the reference to our APC40 instance to an instance variable:

self.control_surface = control_surface

Now all we have to do is override the function on_nested_control_element_value. We’ll do this one step at a time. First, define the function and call its parent’s corresponding function to get the default behavior:

def on_nested_control_element_value(self, value, sender):

parent = super(CustomButtonMatrixElement, self)

parent.on_nested_control_element_value(value, sender)

If you look in the original function that we just overrode, you’ll see how the matrix coordinates for the pressed button are determined. We’ll include that same line in our function so that we can use the x value to determine which track (column) has just been launched:

x, y = self._button_coordinates[sender]

Finally we will iterate through the 8 ‘track stop’ buttons (accessed through self.control_surface), and, as long as its index does not match the index of the column that has just been launched, send it the proper ‘stop’ signal:

session = self.control_surface._session

buttons = session._stop_track_clip_buttons for i, stop_button in enumerate(buttons):

if i != x:

stop_button.receive_value(2)

Using the Customized Scripts

With your APC40 connected, start up Live and go to Preferences. In the MIDI/Sync tab, one of your control surfaces should be set to APC40. If you click the drop down list, you’ll see APC40 custom as a new option. Select it, and you should be good to go. Any custom mappings you have set will still work. To switch back to default behavior, just reselect APC40 from the drop down list.

Disclaimer

Since Ableton’s MIDI Remote Scripts API is undocumented, everything above has been worked out by reading just enough of the decompiled code to make it work. There may be more efficient/direct/sensible ways to achieve the same effect—not to mention entirely different angles of attack, such as using Max for Live. Encouragingly, it also means that the _Framework package is readable enough to achieve pretty fine control without documentation.