Skip to content

ControlSurface lifecycle

The ControlSurface is the root object of every Remote Script: Live creates exactly one per enabled controller and drives it through a fixed lifecycle. Knowing the order of these phases is what separates a script that works from one that crashes on load, leaks listeners, or leaves LEDs lit after you switch it off. This page walks the cradle-to-grave sequence.

TIMEcreate_instance()Live hands youc_instance__init__()with component guard:build elementsbuild components + layersregister LOM listenersRUNNINGevent-driven, not polledMIDI in → componentsLOM changes → listeners→ feedback out (LEDs)update_display() ~ every 100msdisconnect()remove listenersturn off LEDs

Birth: how Live loads your script

Live discovers a Remote Script by folder. Inside that folder, __init__.py exposes a single entry point:

def create_instance(c_instance):
    return MyControlSurface(c_instance)

When you pick the controller in Preferences -> Link/Tempo/MIDI, Live calls create_instance and hands it c_instance -- the bridge object back into the C++ core. You pass it straight to the ControlSurface base class; never throw it away, it is how every LOM call and every MIDI send ultimately reaches Live.

Construction: __init__ and the component guard

All setup happens in __init__. The single most important rule: build everything inside the component guard context manager.

class MyControlSurface(ControlSurface):
    def __init__(self, c_instance, *a, **k):
        super().__init__(c_instance, *a, **k)
        with self.component_guard():
            self._create_controls()      # wrap the physical Elements
            self._create_components()    # SessionComponent, MixerComponent, ...
            self._create_modes()         # wire layers into a ModesComponent

The guard defers side effects until the whole object graph exists, so components can reference each other and listeners are registered safely. Building components outside the guard is the classic source of "half-initialised" bugs.

Order inside the guard matters: create Elements first (they have no dependencies), then Components (which need elements to bind to via layers), then Modes (which arrange components). This mirrors the dependency direction from Components, Layers & Modes.

Going live: the first state push

Once constructed, the surface must paint its initial state onto the hardware -- otherwise LEDs and displays show garbage until the user touches something. Two methods drive this:

  • update() -- asks every enabled component to re-evaluate and re-send its feedback.
  • refresh_state() -- forces a full resend, used after the surface (re)connects or wakes from suspend.

Live also rebuilds the MIDI map at this point (see below), establishing which incoming messages are routed to which elements.

Running: event-driven, never polled

After startup the surface does nothing until something happens. It is purely reactive:

  • a MIDI message arrives -> dispatched to an element -> to the bound control -> to a component handler (the downstream path);
  • a LOM value the surface observes changes -> the listener fires -> the component pushes new feedback out (the upstream path).

There is no per-frame loop you write. If you find yourself wanting one, you almost certainly want a listener instead. The MIDI message flow guide details both directions.

The MIDI map

Live does not send every controller message to Python by default. The surface declares a MIDI map -- which notes/CCs it wants -- and Live routes accordingly. Two paths exist:

  • direct mapping -- Live forwards the message straight to the mapped LOM target with no Python round-trip (low latency, e.g. a fader to a parameter);
  • forwarding -- the message is delivered to your script so a component can interpret it.

When the wiring changes (a mode switch rebinds elements), the map is stale, so the surface calls request_rebuild_midi_map(). Live then calls back into build_midi_map(...), where the current elements re-declare their routing. Rebuilds are batched, not immediate -- request one and let Live call you back.

Suspend and resume

Live suspends a surface when it loses focus or another surface takes over a shared resource, and resumes it afterwards:

  • suspend() / disconnect_... -- stop driving the hardware, but keep the object alive.
  • resume() + refresh_state() -- repaint everything, because the hardware state may have drifted while you were suspended.

Locking to a device (can_lock_to_devices, lock_to_device) is a related mechanism: it lets the surface "follow" the appointed device or pin to a specific one.

Death: disconnect

When the user deselects the controller or quits Live, the surface is torn down via disconnect(). This is not optional cleanup -- it is mandatory:

  • remove every LOM listener you added. Orphaned listeners are the number-one cause of crashes and memory growth across script reloads.
  • turn the hardware off cleanly -- clear LEDs, blank displays, reset the device to a neutral state so it isn't left glowing.
  • call super().disconnect() so the base class releases its own resources.

A useful mental rule: every add_*_listener in construction needs a matching teardown in disconnect. The framework helps -- listeners registered through components are cleaned up when the component is disconnected -- but anything you wire by hand, you unwire by hand.

The lifecycle at a glance

create_instance(c_instance)
        |
        v
   __init__  ──>  with component_guard():  elements -> components -> modes
        |
        v
   update() / refresh_state()        (first paint + build_midi_map)
        |
        v
   RUNNING  <──> (MIDI in / listeners out)      [suspend() <-> resume() as focus changes]
        |
        v
   disconnect()      remove listeners, blank hardware, super().disconnect()

See also: Architecture for the layers this sits on top of, and MIDI message flow for what happens during the RUNNING phase.