Skip to content

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:

Remote Scripts/
└── GenericController/
    ├── __init__.py
    └── GenericController.py

__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

  1. Restart Live (new package → discovered at startup).
  2. 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.
  3. You should see GenericController loaded in 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 wrong CHANNEL, 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.