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.
Navigate tracks and fire a clip¶
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¶
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 yourcomponent_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:
SessionComponentplus aSkindoes 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.
Blink an LED in time with the transport¶
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.