From idea to script¶
This is the page the reference can't give you: a complete control surface, from the idea to running code, built one decision at a time.
Shortcut for this exact case
The walkthrough below — 8 knobs on the selected device + transport — is also what the Script Generator produces with a few clicks. Use the generator if you want a working script now; read on if you want to understand and customise what it does under the hood.
It assumes you have a script that loads (see Your first script) and the mental model from the Architecture overview.
We will build a script for a generic MIDI controller, the kind of cheap box with eight knobs and a few buttons that almost everyone has in a drawer. By the end it will:
- map its eight knobs to the eight parameters of the currently selected device (the "blue hand" follows your selection in Live);
- use two buttons as Play and Stop;
- (stretch) light up and launch a small clip grid.
The whole thing is built from three framework Components wired to Control Elements, the exact pattern described in Components, Layers & Modes and Controls & Elements.
Step 0, the idea, turned into a plan¶
Before writing anything, name what each Component does and which Element feeds it. This five-minute step is what separates a script that works from one you fight for hours.
idea framework Component Control Elements
----------------------- ------------------------- ------------------------
8 knobs -> device params DeviceComponent 8 x EncoderElement (CC)
Play / Stop buttons TransportComponent 2 x ButtonElement (Note)
clip grid (stretch) SessionComponent ButtonMatrixElement (Notes)
You also need to know your hardware's MIDI: which channel it sends on, which CC numbers the knobs send, and which notes the buttons send. Find these with any MIDI monitor, or with the recipe in the Cookbook that logs incoming MIDI. For this walkthrough we assume channel 1, knobs on CC 16–23, and buttons on notes 60 and 61, change the constants to match your box.
Step 1, the controls (Elements)¶
Elements are thin objects that say "this MIDI message is a knob / a button". They hold no behaviour; they are the wires you later plug into Components.
import Live
from _Framework.EncoderElement import EncoderElement
from _Framework.ButtonElement import ButtonElement
from _Framework.InputControlElement import MIDI_CC_TYPE, MIDI_NOTE_TYPE
CHANNEL = 0 # MIDI channel 1 (zero-indexed)
ENCODER_CCS = (16, 17, 18, 19, 20, 21, 22, 23)
PLAY_NOTE = 60
STOP_NOTE = 61
encoders = [
EncoderElement(MIDI_CC_TYPE, CHANNEL, cc, Live.MidiMap.MapMode.absolute)
for cc in ENCODER_CCS
]
play_button = ButtonElement(True, MIDI_NOTE_TYPE, CHANNEL, PLAY_NOTE)
stop_button = ButtonElement(True, MIDI_NOTE_TYPE, CHANNEL, STOP_NOTE)
Two things worth understanding now rather than later: the True first argument to
ButtonElement is is_momentary, True means the pad sends a value on press and on
release (almost always what you want). And MapMode.absolute means each knob sends its
position 0–127 directly; if yours are endless/relative encoders, you will use a relative map
mode instead, see the Cookbook.
Step 2, knobs to the selected device¶
The DeviceComponent does all the hard work of binding eight controls to whatever device is
"appointed" in Live, and of re-binding when you select a different device. You give it the
eight encoders and register it so it follows selection.
from _Framework.DeviceComponent import DeviceComponent
device = DeviceComponent()
device.set_parameter_controls(tuple(encoders))
self.set_device_component(device) # makes it track the appointed ("blue hand") device
set_device_component is a ControlSurface method: it tells the framework "this component
represents the appointed device", so it follows the blue hand and handles the parameter
banking for you. You did not write a single MIDI handler, that is the framework earning its
keep.
Step 3, transport¶
TransportComponent exposes Play, Stop, Record and more. Wire the two buttons:
from _Framework.TransportComponent import TransportComponent
transport = TransportComponent()
transport.set_play_button(play_button)
transport.set_stop_button(stop_button)
That is the whole transport. The component observes the song's playing state, so if you also gave it feedback-capable buttons the Play LED would track Live's actual state, upstream flow, exactly as described in MIDI message flow.
Step 4, assemble the script¶
Now fold Steps 1–3 into the ControlSurface skeleton. Note the component_guard()
context: all Component and Element creation must happen inside it, so the framework can manage
their lifecycle and teardown correctly (see ControlSurface lifecycle).
Structure it as a package:
__init__.py:
from __future__ import absolute_import, unicode_literals
from .GenericController import GenericController
def create_instance(c_instance):
return GenericController(c_instance)
GenericController.py:
from __future__ import absolute_import, print_function, unicode_literals
import Live
from _Framework.ControlSurface import ControlSurface
from _Framework.DeviceComponent import DeviceComponent
from _Framework.TransportComponent import TransportComponent
from _Framework.EncoderElement import EncoderElement
from _Framework.ButtonElement import ButtonElement
from _Framework.InputControlElement import MIDI_CC_TYPE, MIDI_NOTE_TYPE
CHANNEL = 0
ENCODER_CCS = (16, 17, 18, 19, 20, 21, 22, 23)
PLAY_NOTE = 60
STOP_NOTE = 61
class GenericController(ControlSurface):
def __init__(self, c_instance):
super(GenericController, self).__init__(c_instance)
with self.component_guard():
self._create_controls()
self._create_device()
self._create_transport()
self.show_message('GenericController loaded')
self.log_message('GenericController: setup complete')
def _create_controls(self):
self._encoders = [
EncoderElement(MIDI_CC_TYPE, CHANNEL, cc, Live.MidiMap.MapMode.absolute)
for cc in ENCODER_CCS
]
self._play_button = ButtonElement(True, MIDI_NOTE_TYPE, CHANNEL, PLAY_NOTE)
self._stop_button = ButtonElement(True, MIDI_NOTE_TYPE, CHANNEL, STOP_NOTE)
def _create_device(self):
device = DeviceComponent()
device.set_parameter_controls(tuple(self._encoders))
self.set_device_component(device)
def _create_transport(self):
transport = TransportComponent()
transport.set_play_button(self._play_button)
transport.set_stop_button(self._stop_button)
def create_instance(c_instance):
return GenericController(c_instance)
Step 5, load and test¶
- Restart Live (new package → discovered at startup).
- Preferences → Link, Tempo & MIDI → set a Control Surface row to
GenericController, and set its Input (and Output, if your box has LEDs) to your controller's port. - You should see
GenericController loadedin the status bar.
Test checklist:
- Add any device to a track and select it. The eight knobs should move its first eight parameters. Select a different device, the knobs re-bind automatically.
- Press your Play and Stop pads, the transport should respond.
- If nothing moves: open
Log.txt. A traceback points to the line. No traceback but no movement usually means wrongCHANNEL,CC, or that the controller's Input port is not selected.
Stretch, a clip grid with feedback¶
A SessionComponent maps a grid of buttons to the Session view's clip slots, handles launch,
and lights the pads from the clip state, feedback for free. The buttons go into a
ButtonMatrixElement.
from _Framework.SessionComponent import SessionComponent
from _Framework.ButtonMatrixElement import ButtonMatrixElement
NUM_TRACKS = 4
NUM_SCENES = 4
# a 4x4 block of note buttons, e.g. notes 0..15 on CHANNEL
matrix = ButtonMatrixElement()
for row in range(NUM_SCENES):
matrix.add_row([
ButtonElement(True, MIDI_NOTE_TYPE, CHANNEL, row * NUM_TRACKS + col)
for col in range(NUM_TRACKS)
])
session = SessionComponent(NUM_TRACKS, NUM_SCENES)
session.set_clip_launch_buttons(matrix)
Create this inside component_guard() alongside the others. The grid now launches the
clips in the top-left 4×4 of Session view, and the pads light according to whether each slot
has a clip and whether it is playing. The exact LED colours a controller shows depend on its
MIDI implementation (a single brightness, or RGB), mapping those is hardware-specific and is
its own topic; the framework gives you the behaviour, you map the colours.
Validate against your Live version
This example targets _Framework and is written to run on Live 9–11 (and to load on
Live 12). Class and method names in the framework can shift between generations, always
cross-check DeviceComponent, TransportComponent, SessionComponent and the element
classes against the API Reference for the version you are on,
and confirm behaviour by loading the script in Live. This is a starting point to adapt,
not a guaranteed drop-in for every release.
Doing this on Live 12 (v3)
Under ableton.v3.control_surface the same result is expressed declaratively: you
describe controls and a mapping in a Specification and let the framework wire them,
rather than calling set_* methods by hand. The concepts (a device component, a
transport, a session) are the same; the assembly differs. See
v2 vs v3 framework and the Live 12 entries in the
API Reference.
Where to go next¶
You now have the full loop: idea → plan → elements → components → load → test. The Cookbook collects the small recipes that come up constantly, relative encoders, observing the LOM, SysEx, status messages, a shift layer, so you can extend this script without starting from a blank page.