Skip to content

Cookbook

Short, self-contained recipes for things that come up constantly. Each assumes you are inside a ControlSurface subclass (so self is your script and self.song() is the Live set), and that controls are created inside component_guard() as shown in From idea to script. Names follow _Framework; confirm them against the API Reference for your Live version.

See what MIDI your controller sends

If you just want to know which CC / note each control sends, the Script Generator shows them live in the browser — no script, no log. The recipe below is the in-Live equivalent, for when you are already debugging Python.

Before mapping anything, log every incoming message. Drop this into your surface and watch Log.txt:

def receive_midi(self, midi_bytes):
    self.log_message('MIDI in: ' + ' '.join('%02X' % b for b in midi_bytes))
    super(MySurface, self).receive_midi(midi_bytes)

Turn a knob, hit a pad: the status byte tells you Note (0x9n) vs CC (0xBn) and the channel n; the next byte is the note/CC number. Those are the constants you plug into your Elements.

Map a button to any Live action

When no Component does what you want, bind a raw button to your own handler with a value listener. A momentary button sends a non-zero value on press:

def _setup_metronome_button(self, button):
    self._metro_button = button
    button.add_value_listener(self._on_metro)

def _on_metro(self, value):
    if value:                                   # press, ignore release
        self.song().metronome = not self.song().metronome
        self.show_message('Metronome: ' + ('on' if self.song().metronome else 'off'))

The same shape works for anything in the LOM: tap tempo, undo, toggle a track's arm, fire a scene, and so on.

Absolute vs relative encoders

Cheap knobs send absolute position (0–127). Endless encoders send a delta and need a relative map mode, or fast-vs-slow turns misbehave:

import Live
# absolute (0..127 position)
EncoderElement(MIDI_CC_TYPE, ch, cc, Live.MidiMap.MapMode.absolute)
# relative, two's complement (a common endless-encoder mode)
EncoderElement(MIDI_CC_TYPE, ch, cc, Live.MidiMap.MapMode.relative_two_compliment)

If a relative knob jumps or sticks, you have the wrong relative mode, controllers use several conventions (two's complement, signed bit, offset). Try the others, or check your hardware's manual.

Read and observe the LOM

Reading is direct; reacting needs a listener. Always remove listeners you add (see teardown below) or you leak and crash on reload.

# read
self.log_message('tempo is %.2f BPM' % self.song().tempo)

# react to changes
def _setup_tempo_watch(self):
    self.song().add_tempo_listener(self._on_tempo)

def _on_tempo(self):
    self.show_message('tempo -> %.1f' % self.song().tempo)

Most LOM properties follow this add_<name>_listener / remove_<name>_listener pattern. The API Reference lists which properties are observable per version.

song = self.song()
track = song.view.selected_track
slot  = track.clip_slots[0]
if slot.has_clip:
    slot.fire()

# select the next track
tracks = song.tracks
i = list(tracks).index(track)
if i + 1 < len(tracks):
    song.view.selected_track = tracks[i + 1]

Send SysEx or raw MIDI to the controller

For LED rings, displays, or mode handshakes the hardware expects. ControlSurface exposes a low-level send:

# a SysEx message as a byte tuple, including F0 ... F7
self._send_midi((0xF0, 0x47, 0x7F, 0x00, 0xF7))

Many controllers need a SysEx "host mode" message at startup and a reset on teardown, send the first in your setup, the second in disconnect() (below).

A shift / mode layer

To make pads do different things in different modes, the framework way is a ModesComponent (map a button to switch modes, each mode binds controls differently). The quick-and-dirty way is a boolean you check in your handlers:

def _on_shift(self, value):
    self._shift = bool(value)

def _on_pad(self, value):
    if not value:
        return
    if self._shift:
        self._do_alternate_action()
    else:
        self._do_normal_action()

For anything beyond one shift button, prefer ModesComponent, see Components, Layers & Modes.

Status-bar messages

self.show_message('Loaded, 8 knobs on selected device')

Good for load confirmation, mode changes, and "you just hit the bank limit" feedback. It is the cheapest debugging aid after Log.txt.

Clean teardown

Every listener you add must be removed, or Live will throw on the next reload. Override disconnect():

def disconnect(self):
    if self.song().tempo_has_listener(self._on_tempo):
        self.song().remove_tempo_listener(self._on_tempo)
    # reset the controller's LEDs / leave host mode here, e.g.:
    # self._send_midi((0xF0, 0x47, 0x7F, 0x00, 0xF7))
    super(MySurface, self).disconnect()

Components created inside component_guard() are torn down for you; manual listeners and hardware state are your responsibility. This is the single most common source of "it worked once then broke on reload", see ControlSurface lifecycle.

Map encoders to the selected device

The classic "my 8 knobs control whatever device is selected". The cleanest hook is song.appointed_device (the device Live considers "blue-hand" controlled), and its listener fires whenever the user selects another device:

def _setup_device_knobs(self, encoders):
    self._device_encoders = encoders            # a list/tuple of EncoderElement
    self.song().add_appointed_device_listener(self._on_device_changed)
    self._rebind_device()

def _on_device_changed(self):
    self._rebind_device()

def _rebind_device(self):
    device = self.song().appointed_device       # may be None (no device selected)
    params = list(device.parameters)[1:] if device else []   # [0] is always Device On/Off
    for i, enc in enumerate(self._device_encoders):
        enc.release_parameter()                  # drop any previous mapping
        if i < len(params):
            enc.connect_to(params[i])            # direct hardware-to-parameter mapping

connect_to / release_parameter on an EncoderElement give you motorised, takeover-correct control for free, no value listener needed. For absolute knobs that is enough; for endless encoders make sure the element was created with a relative map mode (see above).

v3 note: you rarely write this by hand. A DeviceComponent (mapped in your component_map) already follows the appointed device and banks parameters in groups of eight.

Show a parameter's value as text

DeviceParameter knows how to format itself the way Live's own display does, via str_for_value, which beats formatting floats yourself (it handles dB, %, Hz, enums):

def _on_param_value(self, parameter):
    text = parameter.str_for_value(parameter.value)   # e.g. "-6.0 dB", "27 %", "Sine"
    self.show_message('%s: %s' % (parameter.name, text))

# wire it up (and remember to remove it in disconnect):
# parameter.add_value_listener(lambda p=parameter: self._on_param_value(p))

parameter.min and parameter.max give the range if you need to scale an LED ring or a meter. parameter.is_enabled tells you whether the control should respond at all.

Colour a pad grid from clip state

The heart of any Session view: walk the slots and light each pad by what the clip is doing. ClipSlot exposes has_clip, is_playing and is_triggered directly, no need to reach into the clip for the basics:

def _update_grid(self):
    tracks = self.song().tracks
    for col, button_column in enumerate(self._pad_grid):     # your 2-D button layout
        track = tracks[col] if col < len(tracks) else None
        for row, button in enumerate(button_column):
            slot = track.clip_slots[row] if track and row < len(track.clip_slots) else None
            button.send_value(self._slot_color(slot), force=True)

def _slot_color(self, slot):
    if slot is None or not slot.has_clip:
        return 0                                  # off
    if slot.is_triggered:
        return 5                                  # blinking-ish (your controller's value)
    if slot.is_playing:
        return 21                                 # green on your device's colour table
    return 1                                       # has clip, stopped

The integers are whatever your hardware's colour table uses, the APC, Launchpad and Push all differ. To keep it live, add add_has_clip_listener / add_playing_status_listener on each slot and call _update_grid from them (and remove them all in disconnect).

v3 note: SessionComponent plus a Skin does this declaratively, you ask for a named colour (Session.ClipStopped) and the skin maps it to the right MIDI value per device. See the Skin recipe below.

Use the framework's own scheduler (Task), never time.sleep (it would freeze Live). Tasks live on self._tasks in a ControlSurface:

from _Framework.Task import sequence, wait, run

def _start_blink(self, button):
    self._blink_button = button
    self._blink_on = False
    self._blink_task = self._tasks.add(sequence(
        run(self._toggle_blink),
        wait(0.25),                      # seconds; 0.25 = 16th notes at 120 BPM
    ))
    self._blink_task.repeat()

def _toggle_blink(self):
    self._blink_on = not self._blink_on
    self._blink_button.send_value(21 if self._blink_on else 0, force=True)

For true musical sync (follow tempo changes), drive the toggle from song.add_current_song_time_listener and look at the beat fraction instead of a fixed wait. Kill the task in disconnect() with self._blink_task.kill().

A proper modes component

The boolean-shift trick above is fine for one button. For real mode switching (each mode re-binds a whole set of controls) the framework way is ModesComponent: you add named modes, each made of layers, and tell it which button toggles them.

from _Framework.ModesComponent import ModesComponent, AddLayerMode
from _Framework.Layer import Layer

def _setup_modes(self, session, mixer, mode_button):
    modes = ModesComponent(name='Main_Modes')
    modes.add_mode('clips',  AddLayerMode(session, Layer(clip_launch_buttons=self._pads)))
    modes.add_mode('mixer',  AddLayerMode(mixer,   Layer(volume_controls=self._faders)))
    modes.selected_mode = 'clips'                 # default
    modes.set_toggle_button(mode_button)          # one button cycles modes
    self._main_modes = modes

Each mode owns its layer, so switching mode cleanly disconnects the previous bindings and connects the new ones, no leftover state, no LEDs stuck from the other mode. This is the single biggest step up from hand-wired scripts.

v3 note: modes are declared in the specification / create_mappings, with the same mental model (named modes, each binding a set of controls). See v2 vs v3.