from sideman import Live
live = Live()
live.count("live_set", "tracks"){'path': 'live_set', 'child': 'tracks', 'count': 3}
A Pad track on Drift playing Fm7 – Db – Ab – Eb, one chord per bar, voiced close around C4 with the seventh only on the i. Four chords is the whole harmonic content of the track; everything after this lesson is decoration on top of it.
Prerequisites: lesson 3 executed — Drums, Sub and Mid at tracks 0–2.
The ask: “Add a Pad on Drift with a warm preset. Play Fm7, Db, Ab, Eb — one bar each, close voicings around C4. Keep the velocities gentle.”
from sideman import Live
live = Live()
live.count("live_set", "tracks"){'path': 'live_set', 'child': 'tracks', 'count': 3}
set_batch sends many writes down one connection and puts them in one undo step. It is the blunt version of transaction: no call ops, just properties. Name and colour before the preset, for the reason lesson 2 gave.
live.call("live_set", "create_midi_track", [-1])
live.set_batch([
{"path": "live_set tracks 3", "property": "name", "value": "Pad"},
{"path": "live_set tracks 3", "property": "color_index", "value": 42},
]){'count': 2,
'applied': 2,
'failed': False,
'results': [{'index': 0,
'op': 'set',
'ok': True,
'result': {'path': 'live_set tracks 3',
'property': 'name',
'value': 'Pad'}},
{'index': 1,
'op': 'set',
'ok': True,
'result': {'path': 'live_set tracks 3',
'property': 'color_index',
'value': 42}}],
'undo': 'Groupable ops collapse into ONE Cmd-Z. Automatable parameters (tempo, mixer volume, mute, device parameters) each form their OWN undo step in Live regardless of grouping, so reverting those needs one extra undo apiece. Measured on Live 12.2.7.'}
“Warm” is not a property Live exposes, so this is the one step a model cannot verify for you: read the names, pick one, listen. Drift’s pad folder has 48 entries; these are the flat presets.
[i["name"] for i in live.browser_list("instruments/Drift/Pad")["items"]
if i["name"].endswith(".adv")]['Bloom Pad.adv',
'Flutter Pad.adv',
'Foglie Pad.adv',
'Gentle Pulse Pad.adv',
'Light Frost.adv',
'Metallic Flutter Pad.adv',
'Metallic Swell Pad.adv',
'Monday Morning.adv',
'Morning Chorus Pad.adv',
'Rising Fog Pad.adv',
'Scape Shape Pad.adv',
'Shark Pad.adv',
'Star Talk Pad.adv',
'Sweet Tooth.adv',
'Synth on Screen.adv',
'Wave Pad.adv']
live.browser_load("instruments/Drift/Pad/Morning Chorus Pad.adv", track_index=3){'loaded': 'instruments/Drift/Pad/Morning Chorus Pad.adv', 'track': 'Pad'}
browser_load just selected that track, so live_set view selected_track now points at the Pad. That path is an alias: it means “whatever is selected”, which changes the moment the user clicks elsewhere. canonical_path resolves it to the form worth storing.
live.canonical_path("live_set view selected_track"){'path': 'live_set view selected_track',
'type': 'Track.Track',
'canonical_path': 'live_set tracks 3',
'is_alias': True,
'nodes_visited': 5}
i – VI – III – WII in F minor: Fm7, Db, Ab, Eb. The voicings sit between Eb3 and F4 and move as little as possible — F3 and Ab3 are held across the first two bars, Ab3 across the next, Eb across the last two. Close voicing around C4 is what keeps a pad out of the way of the bass below it and the lead above it.
Velocities 62–75. A pad played hard stops being a pad.
PAD = "live_set tracks 3 clip_slots 0 clip"
live.call("live_set tracks 3 clip_slots 0", "create_clip", [16.0])
chords = [(0.0, "Fm7", [53, 56, 60, 63], 0), # F3 Ab3 C4 Eb4
(4.0, "Db", [53, 56, 61, 65], -4), # F3 Ab3 Db4 F4
(8.0, "Ab", [51, 56, 60, 63], 0), # Eb3 Ab3 C4 Eb4
(12.0, "Eb", [51, 55, 58, 63], 3)] # Eb3 G3 Bb3 Eb4
voice_vel = [66, 62, 64, 72]
notes = [{"pitch": p, "start_time": start, "duration": 3.5,
"velocity": voice_vel[i] + lift}
for start, _, pitches, lift in chords
for i, p in enumerate(pitches)]
live.notes_add(PAD, notes){'path': 'live_set tracks 3 clip_slots 0 clip', 'added': 16}
g = live.notes_get(PAD, time_span=4.0)
g["count"], [(n["pitch"], n["velocity"]) for n in g["notes"]](4, [(53, 66.0), (56, 62.0), (60, 64.0), (63, 72.0)])
Volume and a reverb send. Both are mixer parameters on the track’s mixer_device, both normalised 0–1, and sends 0 is the A return — the send knows its own name, so check it rather than counting returns.
live.get("live_set tracks 3 mixer_device sends 0", "name"){'path': 'live_set tracks 3 mixer_device sends 0',
'property': 'name',
'value': 'A-Reverb',
'type': 'builtins.str'}
live.set_batch([
{"path": "live_set tracks 3 mixer_device volume", "property": "value", "value": 0.78},
{"path": "live_set tracks 3 mixer_device sends 0", "property": "value", "value": 0.25},
]){'count': 2,
'applied': 2,
'failed': False,
'results': [{'index': 0,
'op': 'set',
'ok': True,
'result': {'path': 'live_set tracks 3 mixer_device volume',
'property': 'value',
'value': 0.7799999713897705}},
{'index': 1,
'op': 'set',
'ok': True,
'result': {'path': 'live_set tracks 3 mixer_device sends 0',
'property': 'value',
'value': 0.25}}],
'undo': 'Groupable ops collapse into ONE Cmd-Z. Automatable parameters (tempo, mixer volume, mute, device parameters) each form their OWN undo step in Live regardless of grouping, so reverting those needs one extra undo apiece. Measured on Live 12.2.7.'}
[live.get(f"live_set tracks 3 mixer_device {p}", "value")["value"]
for p in ("volume", "sends 0", "panning")][0.7799999713897705, 0.25, 0.0]
0.78 comes back as 0.7799999713897705. Live stores parameter values as 32-bit floats, so a value you wrote is not always a value you can compare with ==. Round before you assert.
Undo grouping is partial. The name and the colour above collapsed into one ⌘Z; the volume and the send did not — they are automatable parameters, and Live gives those their own undo step whatever you wrap them in. set_batch and transaction both say so in their undo field. Saying “one undo for the names, one each for the mixer” is honest; saying “one undo” is not.
An alias path is not a bug. live_set view selected_track, master_track, visible_tracks — several paths reach the same object, and which one you keep matters: store the canonical form, use the alias only to find out what the user is pointing at.
assert live.count("live_set", "tracks")["count"] == 4
assert live.get("live_set tracks 3", "name")["value"] == "Pad"
assert live.notes_get(PAD)["count"] == 16
assert round(live.get("live_set tracks 3 mixer_device volume", "value")["value"], 3) == 0.78
assert live.canonical_path("live_set view selected_track")["canonical_path"] == \
"live_set tracks 3"
print("lesson 4 ok — Pad at track 3, Fm7 Db Ab Eb, 16 notes")lesson 4 ok — Pad at track 3, Fm7 Db Ab Eb, 16 notes