from sideman import Live
live = Live()
live.get("live_set", "tempo"){'path': 'live_set',
'property': 'tempo',
'value': 122.0,
'type': 'builtins.float'}
One MIDI track, a 909 kit, and 88 notes: four-on-the-floor kick, clap on 2 and 4, open hats on the off-beats, closed hats on the remaining 16ths. Four bars, looping. By the end of this lesson the Set contains exactly one track — yours — and something you can dance to.
Prerequisites: lesson 1 executed against this Set (122 BPM, F minor, the four default tracks still there).
The ask: “Make a Drums track with a 909 kit. Kick on every beat, claps on 2 and 4, open hats off-beat, closed hats on 16ths with a little velocity movement. Four bars, then loop it.”
from sideman import Live
live = Live()
live.get("live_set", "tempo"){'path': 'live_set',
'property': 'tempo',
'value': 122.0,
'type': 'builtins.float'}
create_midi_track is a name from describe("live_set")["functions"]. There is no create_track sugar anywhere in Sideman — the function list is the API. -1 appends; an index inserts there.
live.call("live_set", "create_midi_track", [-1]){'path': 'live_set',
'function': 'create_midi_track',
'result': {'__lom__': 'Track.Track', 'name': '5-MIDI'}}
Live renames a track when you load a preset onto it — but only while the name is still the default. Set the name first and it survives the kit. Two writes in one cell, and a notebook prints only the last expression, so the colour is what comes back.
browser_list walks Live’s library by name. The drums folder is long, so filter it in Python rather than reading 215 lines of output. Kits are real library files, which is why the name ends in .adg.
[i["name"] for i in live.browser_list("drums")["items"] if "909" in i["name"]]['909 Core Kit.adg']
live.browser_load("drums/909 Core Kit.adg", track_index=4){'loaded': 'drums/909 Core Kit.adg', 'track': 'Drums'}
The result reports which track it landed on. Without track_index it loads onto whatever track is selected, which is rarely what you meant.
A new Set hands you two MIDI and two audio tracks. We are building from scratch, so they go. Two things to notice: deletion is destructive, so it refuses to run without confirm=True, and it reindexes — delete the highest index first or the ground moves under you.
for i in (3, 2, 1, 0):
live.call("live_set", "delete_track", [i], confirm=True)
live.count("live_set", "tracks")["count"], live.get("live_set tracks 0", "name")["value"](1, 'Drums')
The Drums track was at index 4; it is now index 0, and every path below is built from that. One more thing changed: with an instrument on it, this MIDI track now reports output meters that it did not have when it was empty — compare lesson 1’s unavailable list.
sorted(live.describe("live_set tracks 0", include_values=False)["unavailable"])['fold_state', 'input_meter_left', 'input_meter_right']
A Drum Rack is a device whose pads each hold a chain. The pads know their own MIDI note, so you never have to remember which number is the clap.
DEV = "live_set tracks 0 devices 0"
pads = {live.get(f"{DEV} visible_drum_pads {i}", "note")["value"]:
live.get(f"{DEV} visible_drum_pads {i}", "name")["value"]
for i in range(live.count(DEV, "visible_drum_pads")["count"])}
{p: pads[p] for p in (36, 39, 42, 46)}{36: 'Bass Drum', 39: 'Hand Clap', 42: 'Closed Hi Hat', 46: 'Open Hi Hat'}
A clip_slot always exists; the clip inside it does not until you create one. 16 beats is 4 bars in 4/4.
CLIP = "live_set tracks 0 clip_slots 0 clip"
live.call("live_set tracks 0 clip_slots 0", "create_clip", [16.0]){'path': 'live_set tracks 0 clip_slots 0',
'function': 'create_clip',
'result': {'__lom__': 'Clip.Clip', 'name': ''}}
[live.get(CLIP, p)["value"] for p in ("length", "looping", "loop_start", "loop_end")][16.0, True, 0.0, 16.0]
Notes are one of the few typed operations. Live hands back MidiNote objects but wants MidiNoteSpecification on write, and generic get/set cannot express the difference — hence notes_add rather than set(clip, "notes", ...).
Times are in beats from the clip start, pitch is a MIDI note number, velocity is 1–127. The pattern is a comprehension, which is the whole reason to do this from a notebook.
def note(pitch, start, dur, vel):
return {"pitch": pitch, "start_time": start, "duration": dur, "velocity": vel}
kick = [note(36, b, 0.25, 112) for b in range(16)]
clap = [note(39, b, 0.30, 96) for b in range(1, 16, 2)]
open_hat = [note(46, b + 0.5, 0.35, 84) for b in range(16)]
closed = [note(42, b + off, 0.15, 96 if off == 0.0 else 72)
for b in range(16) for off in (0.0, 0.25, 0.75)]
len(kick), len(clap), len(open_hat), len(closed)(16, 8, 16, 48)
The closed hat sits out at +0.5 on every beat: that is where the open hat plays, and two hats on the same 16th is mud, not groove. On the beat it hits 96, between the beats 72 — enough movement that the pattern breathes without sounding programmed.
One call writes all of it.
live.notes_add(CLIP, kick + clap + open_hat + closed){'path': 'live_set tracks 0 clip_slots 0 clip', 'added': 88}
g = live.notes_get(CLIP)
g["count"], g["notes"][:3](88,
[{'note_id': 1,
'pitch': 36,
'start_time': 0.0,
'duration': 0.25,
'velocity': 112.0,
'mute': False,
'probability': 1.0,
'velocity_deviation': 0.0,
'release_velocity': 64.0},
{'note_id': 2,
'pitch': 36,
'start_time': 1.0,
'duration': 0.25,
'velocity': 112.0,
'mute': False,
'probability': 1.0,
'velocity_deviation': 0.0,
'release_velocity': 64.0},
{'note_id': 3,
'pitch': 36,
'start_time': 2.0,
'duration': 0.25,
'velocity': 112.0,
'mute': False,
'probability': 1.0,
'velocity_deviation': 0.0,
'release_velocity': 64.0}])
Keep the note_ids. They are how you edit a note in place instead of deleting and rewriting it.
live_set swing_amount is not it — that setting swings quantisation when you record, and does nothing to notes already in a clip. Swing in a programmed pattern means the notes are late. 0.02 beats is about 10 ms at 122 BPM: under the threshold where you hear it as a mistake, over the one where you feel it.
late = [{"note_id": n["note_id"], "start_time": n["start_time"] + 0.02}
for n in live.notes_get(CLIP)["notes"]
if n["pitch"] == 42 and n["start_time"] % 0.5 != 0]
len(late), live.notes_modify(CLIP, late)(32,
{'path': 'live_set tracks 0 clip_slots 0 clip',
'modified': 32,
'unmatched_note_ids': []})
[n["start_time"] for n in live.notes_get(CLIP, from_pitch=42, pitch_span=1,
time_span=4.0)["notes"]][0.0,
0.270000052031302,
0.7700000520313021,
1.0,
1.270000052031302,
1.770000052031302,
2.0,
2.270000052031302,
2.770000052031302,
3.0,
3.270000052031302,
3.770000052031302]
0.2700000520 rather than 0.27: Live stores note positions at its own resolution, which is finer than anything you can hear and never exactly what you typed. Compare note times with a tolerance, or by their order.
unmatched_note_ids is empty, so every edit landed. If a note had been deleted between the read and the write, it would be listed there rather than failing the whole call.
Firing the clip slot launches the clip; start_playing guarantees the transport is rolling. Listen to it before you build anything on top.
live.call("live_set tracks 0 clip_slots 0", "fire")
live.call("live_set", "start_playing")
live.get("live_set", "is_playing")["value"]True
live.call("live_set", "stop_playing") # the clip stays; the room goes quiet{'path': 'live_set', 'function': 'stop_playing', 'result': None}
Notes cannot round-trip through generic get/set. Everything else in this lesson — the track, the kit, the clip, its length — is plain property access on a path, and that is the point of a generic server: the typed clip_* tools exist only for the handful of shapes JSON genuinely cannot carry.
Times are beats, not bars and not seconds. Pitch is a MIDI number, and Live writes C3 = 60 where some other tools write C4 = 60. The Drum Rack’s pads tell you which number is which sound, so ask the pads instead of counting.
assert live.count("live_set", "tracks")["count"] == 1
assert live.get("live_set tracks 0", "name")["value"] == "Drums"
assert live.get(CLIP, "length")["value"] == 16.0
assert live.get(CLIP, "looping")["value"] is True
assert live.notes_get(CLIP)["count"] == 88
assert live.get("live_set", "tempo")["value"] == 122.0
print("lesson 2 ok — Drums at track 0, 88 notes over 4 bars, looping")lesson 2 ok — Drums at track 0, 88 notes over 4 bars, looping