from sideman import Live
live = Live()
live.ping(){'pong': True, 'port': 9878, 'handlers': True, 'handler_error': None}
Two writes and a dozen reads. By the end of this lesson the Set is at 122 BPM in F minor, and you can answer “what is in here?” with a query instead of a look at the screen. Nothing musical happens yet; what you gain is the address system every later lesson uses.
Prerequisites: Live open on a fresh Set (⌘N), AbletonLOM selected as a Control Surface in Preferences → Link, Tempo & MIDI, and this repo’s venv as the kernel. No earlier lesson required — this is the first.
The ask: “Set the tempo to 122 and the key to F minor. Then tell me what’s in this Set.”
from sideman import Live
live = Live()
live.ping(){'pong': True, 'port': 9878, 'handlers': True, 'handler_error': None}
handlers: True means the engine imported. If it is False, read handler_error — the Remote Script is loaded but its ops are not.
describedescribe at a path returns three things: properties with their values, children you can navigate into, and functions you can call. Nothing is hidden behind a wrapper that has to be kept up to date, so nothing goes stale when Live ships a new version.
d = live.describe("live_set", include_values=False)
d["counts"], d["children"]({'properties': 52, 'children': 6, 'functions': 186, 'unavailable': 0},
{'cue_points': 0,
'return_tracks': 2,
'scale_intervals': 7,
'scenes': 8,
'tracks': 4,
'visible_tracks': 4})
Those six children are the whole top level of a Set: the tracks, the scenes, the two return tracks, the locators (cue_points, empty so far), and scale_intervals — which is a child rather than a property because it is a list.
Tempo is one float on live_set. Key is two properties: root_note, a pitch class where C is 0, and scale_name. Setting them transposes nothing — it tells Live, and anything reading Live, what the track is in. Live 12’s scale-aware MIDI editing follows it.
live.set("live_set", "tempo", 122){'path': 'live_set', 'property': 'tempo', 'value': 122.0}
live.set("live_set", "root_note", 5) # 0 = C, so 5 = F{'path': 'live_set', 'property': 'root_note', 'value': 5}
live.set("live_set", "scale_name", "Minor"){'path': 'live_set', 'property': 'scale_name', 'value': 'Minor'}
[live.get("live_set", p)["value"]
for p in ("tempo", "root_note", "scale_name", "scale_mode")][122.0, 5, 'Minor', True]
live.get("live_set", "scale_intervals")["value"][0, 2, 3, 5, 7, 8, 10]
Semitones from the root: the natural minor scale. Lesson 5 reads this list to build the melody instead of hardcoding F minor, which is the difference between a lesson and a script.
A fresh Set is not empty. Count before you assume.
{child: live.count("live_set", child)["count"]
for child in ("tracks", "scenes", "return_tracks", "cue_points")}{'tracks': 4, 'scenes': 8, 'return_tracks': 2, 'cue_points': 0}
[(i,
live.get(f"live_set tracks {i}", "name")["value"],
live.get(f"live_set tracks {i}", "has_midi_input")["value"])
for i in range(live.count("live_set", "tracks")["count"])][(0, '1-MIDI', True),
(1, '2-MIDI', True),
(2, '3-Audio', False),
(3, '4-Audio', False)]
Two MIDI tracks, two audio tracks, the names Live gives them. Lesson 2 builds the drums from scratch and deletes all four.
The same describe works one level down. Read unavailable: those members exist on the type but not on this instance. This MIDI track, with nothing loaded on it, reports neither input meters (no audio comes in) nor output meters (it outputs MIDI, not audio). That is Live being correct, not an error, and it is the reason to ask rather than guess. Lesson 10 leans on this map when it puts observers on meters.
t = live.describe("live_set tracks 0", include_values=False)
t["counts"], sorted(t["unavailable"])({'properties': 35, 'children': 8, 'functions': 151, 'unavailable': 5},
['fold_state',
'input_meter_left',
'input_meter_right',
'output_meter_left',
'output_meter_right'])
sorted(live.describe("live_set tracks 2", include_values=False)["unavailable"])['fold_state']
search walks for you, filtered by name, by type, or both. It is bounded on purpose — an unbounded walk of a large Set would freeze Live’s UI — so check truncated.
hits = live.search(type="Track")
hits["count"], hits["truncated"], [r["path"] for r in hits["results"]](14,
False,
['live_set tracks 0',
'live_set tracks 1',
'live_set tracks 2',
'live_set tracks 3',
'live_set return_tracks 0',
'live_set return_tracks 1',
'live_set master_track',
'live_set tracks 0 view',
'live_set tracks 1 view',
'live_set tracks 2 view',
'live_set tracks 3 view',
'live_set return_tracks 0 view',
'live_set return_tracks 1 view',
'live_set master_track view'])
Fourteen paths for four tracks. Seven of them are tracks — the two returns and the main track are Tracks as well. The other seven are each track’s view, because type is matched as a substring and Track.View contains “Track”. The filter is cheap and blunt on purpose: read the paths, do not trust the count.
The census behind all of this is one call. It is large — the per-type member lists are the map of everything addressable — so describe remains the tool for day-to-day navigation. COVERAGE.md explains what the totals count and how they are re-measured after a Live release.
census = live.types()
census["type_count"], census["totals"](47,
{'mxd': 785,
'union': 2227,
'substantive': 922,
'listeners': 1305,
'registered_types': 43,
'unregistered_types': 4})
Paths are space-separated and zero-indexed: live_set tracks 0 mixer_device volume. No quotes, no dots, no brackets. Every path in these ten lessons is built by string formatting, which is why a track index in a variable is enough to address anything on it.
describe is how you learn what exists rather than guessing. A guess costs a failed round trip and a traceback; describe costs one round trip and is right. When a member you remember is missing, describe is the source of truth and your memory is the thing that is wrong.
Re-run this cell any time. If it raises, the Set drifted from what lesson 2 expects.
assert live.get("live_set", "tempo")["value"] == 122.0
assert live.get("live_set", "root_note")["value"] == 5
assert live.get("live_set", "scale_name")["value"] == "Minor"
assert live.count("live_set", "tracks")["count"] == 4
assert live.get("live_set", "file_path")["value"] == "", "save the Set only when you mean to"
print("lesson 1 ok — 122 BPM, F minor, 4 default tracks")lesson 1 ok — 122 BPM, F minor, 4 default tracks