Appendix A — Syllabus
Ten lessons that build one track — 122 BPM, F minor, five and a half minutes, intro → build → drop → breakdown → second drop → outro — and along the way touch every one of Sideman’s tools where a producer would actually reach for it. Each lesson leaves a checkable state in the Set, so you can verify where you are with a query rather than a feeling.
The course has two parts for two readers. Make a track with Claude is for musicians: what to say, what happens in Live, how to check — no code, starting with three prompts you can try in ten minutes. Under the hood is for technical readers: the same lessons as executed notebooks driving the Python client, every cell’s output frozen from a real Set. The syllabus below serves both; each lesson lists the ask and, for the second part, underneath — the calls behind it.
Prerequisites: Sideman installed, Live open on a fresh Set (⌘N), Claude Code with the sideman skill. Nothing in a lesson touches a Set you did not build in the previous one.
| # | Lesson | Musical outcome | Tools introduced |
|---|---|---|---|
| 1 | Orientation | tempo, key, an empty Set you can read | lom_ping lom_describe lom_search lom_get lom_set lom_count lom_types |
| 2 | Drums | kick, hats, clap, a groove | browser_list browser_load clip_add_notes clip_get_notes clip_modify_notes lom_call |
| 3 | Bass | sub and mid bass locked to the kick | lom_transaction clip_envelope_insert_step clip_envelope_get |
| 4 | Harmony | a four-chord progression on a pad | lom_set_batch lom_canonical_path |
| 5 | Melody and arp | a plucked lead and an arpeggio | clip_remove_notes lom_get_batch |
| 6 | Sound design | racks, macros, sends, a plug-in | lom_call with __path__, lom_get paging |
| 7 | Movement | filter builds, reverb throws, risers | clip_envelope_clear |
| 8 | Arrangement | the full 5:30 structure with locators | arrangement_create_clip arrangement_duplicate_clip arrangement_list_clips |
| 9 | Audio | a vocal chop, warped and placed | clip_set_warp_markers |
| 10 | Mixing with a human in the room | levels, sends, and reacting to what you touch | lom_observe lom_poll_events lom_observers lom_unobserve lom_unobserve_all |
A.1 1. Orientation — the Set is a graph
Outcome: 122 BPM, F minor, and a mental model of paths.
The ask: “Set the tempo to 122 and the key to F minor. Then tell me what’s in this Set.”
Underneath: lom_set("live_set", "tempo", 122); lom_set("live_set", "root_note", 5) and scale_name "Minor"; lom_describe("live_set") for the children (tracks, scenes, return_tracks, master_track); lom_count on each. lom_search(type="Track") returns 14 paths for 4 tracks — the two returns and the main track are Tracks too, and type is matched as a substring, so each track’s Track.View matches as well. lom_types once, to see every type the census measured.
Lesson inside the lesson: paths are space-separated and zero-indexed — live_set tracks 0 mixer_device volume — and lom_describe is how Claude learns what exists rather than guessing. A guess costs a failed round trip; describe costs one and is right.
Checkpoint: lom_get("live_set", "tempo") → 122.0, lom_get("live_set", "scale_name") → "Minor", lom_count("live_set", "tracks") → 4 (a new Set’s defaults). lom_describe on one of those MIDI tracks lists five members under unavailable: no input meters, and no output meters either while the track is empty — those appear once an instrument is on it (lesson 2).
A.2 2. Drums — the kick is the clock
Outcome: a Drums track with a 909-style kit, four-on-the-floor kick, off-beat open hats, clap on 2 and 4, 16th closed hats with alternating velocity. 4 bars, looping, swung slightly.
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.”
Underneath: lom_call("live_set", "create_midi_track", [-1]); browser_list("drums") then browser_load("drums/<kit>", track_index=…); lom_call(clip_slot, "create_clip", [16.0]); one clip_add_notes with the whole pattern (kick 36, clap 39, closed hat 42, open hat 46); clip_get_notes to read back note_ids; clip_modify_notes to push the in-between 16ths 0.02 beats late. Swing is moving notes: live_set swing_amount swings quantisation when you record and does nothing to notes already in a clip.
Lesson inside the lesson: notes cannot round-trip through generic get/set — the typed clip_* tools exist for exactly that. Times are beats, pitch is a MIDI number (C3 = 60).
Checkpoint: clip_get_notes(...) → 88 notes — kick 16, clap 8, open hat 16, closed hat 48. The closed hat sits out at every +0.5, where the open hat plays: two hats on one 16th is mud, not groove. lom_get(clip, "length") → 16.0; lom_get(clip, "looping") → true. Note times come back at Live’s own resolution (0.2700000520, not 0.27), so compare them with a tolerance.
A.3 3. Bass — sub and mid, and the first automation
Outcome: two bass tracks (Sub, Mid) on Drift, root-note pattern locked to the kick, mid bass with a filter that opens over the phrase.
The ask: “Add a Sub and a Mid bass on Drift. Sub plays F on the off-beats under the kick; Mid plays a syncopated F–Ab–C line an octave up. Name and colour both in one undo. Open the Mid’s filter over the four bars.”
Underneath: two create_midi_track, browser_load("instruments/Drift") ×2, lom_transaction for four writes (two names, two color_index), notes, then lom_search(type="DeviceParameter", root=<Mid track>) to find LP Freq — Drift does not call it Cutoff — and clip_envelope_insert_step ×4 (0.35 → 0.5 → 0.72 → 0.95), clip_envelope_get to read it back.
Lesson inside the lesson: device parameters are normalised 0–1; read min/max before writing. Automation is a (clip, parameter) pair — two paths, not one. And this is the capability the Max for Live object model cannot reach; it is why Sideman is a Remote Script.
Checkpoint: clip_envelope_get(...) → 4 steps ending at 0.95; lom_get("live_set", "can_undo") → true and one ⌘Z removes all four name/colour writes together.
A.4 4. Harmony — the progression, in one undo
Outcome: a Pad track with Fm – Db – Ab – Eb (i – VI – III – VII), one chord per bar, voiced in the 3rd–4th octave with the 7th on the i.
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.”
Underneath: browser_list("instruments/Drift") to pick a preset; clip_add_notes with the voiced chords; lom_set_batch to set the pad track’s mixer_device volume and a send in one round trip; lom_canonical_path("live_set view selected_track") to resolve whatever the user has selected into a stable path.
Lesson inside the lesson: undo grouping is partial. Names, colours and time signatures collapse into one ⌘Z through lom_transaction; tempo, volume, mute and device parameters always get their own step — Live decides that, not the server. Claude should say which is which rather than promise “one undo”.
Checkpoint: clip_get_notes(pad clip) → 16 notes across 4 bars, four voices per chord; lom_get("live_set tracks N mixer_device volume", "value") → what was set, to within a float: 0.78 reads back as 0.7799999713897705, because Live stores parameter values as 32-bit floats. Round before comparing.
A.5 5. Melody and arp — the hook
Outcome: a Lead track with a plucked top line (call in bars 1–2, response in 3–4) and an Arp track running a 16th-note arpeggio of each chord.
The ask: “Write a plucky lead melody over the chords — a two-bar call and a two-bar answer, mostly scale tones, one blue note. Then an arp track that runs each chord as rising 16ths. Humanise the arp velocities.”
Underneath: notes derived from the chord tones (F minor scale intervals from lom_get("live_set", "scale_intervals")); clip_modify_notes for velocity humanisation and clip_remove_notes to thin a phrase; lom_get_batch to read every track’s name, colour and clip length in one call before deciding.
Lesson inside the lesson: lom_get_batch isolates failures — one unavailable property does not lose the other reads.
Checkpoint: two new tracks; arp clip has 64 notes (16 per bar); lead clip has 17 after clip_remove_notes thins the second bar; clip_get_notes(lead) shows the blue note (Cb/B, pitch 59 or 71). Notes come back grouped by pitch, not in time order.
A.6 6. Sound design — racks, macros, sends, and the plug-in wall
Outcome: the Lead through a filter and a ping-pong delay into an Audio Effect Rack whose macros are ready and unmapped; sends to the A Reverb and B Delay returns; one third-party plug-in loaded and honestly described. Two of the four steps end at a wall the API does not cross.
The ask: “Put an effect rack on the Lead with a filter and a ping-pong delay, expose cutoff and delay feedback as macros. Send the pad to the reverb return. Load Pro-Q on the master and tell me what you can control.”
Underneath: browser_load("audio_effects/..."), which always appends to the end of the track’s chain — view selected_device is read-only, so a script cannot choose where a device lands, and lom_call("live_set", "move_device", [{"__path__": dev}, {"__path__": track}, 1]) is how the order gets fixed afterwards. A stored device path is a position: after a move, devices 1 is a different device, so re-resolve rather than reusing a variable. lom_call(rack, "add_macro") reveals two macros per call (8 → 10 → 12), and macros_mapped is read-only — see the lesson inside. lom_set("... mixer_device sends 0", "value", 0.4); for the master there is no track index — browser_load loads onto the selected track, so select it first: lom_set("live_set view", "selected_track", {"__path__": "live_set master_track"}) then browser_load("plugins/..."); then lom_get(device, "parameters", limit=25) and lom_call(device, "get_parameter_names", limit=50).
Lesson inside the lesson: two walls, both real. A VST/AU exposes only Device On until the user presses Configure in Live — a GUI-only step with no API. And a macro can be added but not mapped: nothing in the Live Object Model connects Macro 1 to a filter’s cutoff, so macros_mapped stays all false and has_macro_mappings stays false until the user drags. Claude can read the plug-in’s full parameter names and tell the user exactly what to expose and what to map; it cannot press the button or make the drag. Reporting either as done is a lie the user will find in ten seconds. Arguments that are Live objects go in {"__path__": "..."} markers, never as bare strings. Long vectors page with offset/limit.
Checkpoint: lom_count(master, "devices") → 1; lom_get(plugin, "parameters", limit=5) → count 1 before Configure, while lom_call(plugin, "get_parameter_names", limit=12) reports 358 names Live already knows. The Lead’s chain reads Drift, Auto Filter, Echo, rack — the rack last, because the reorder moved the other two in front of it — and lom_get(rack, "macros_mapped") is sixteen false.
A.7 7. Movement — builds, throws, risers
Outcome: a 16-bar build clip set where the pad filter opens, the reverb send rises, and a white-noise riser on a Noise track climbs to the drop.
The ask: “Make a 16-bar build: pad filter closed to open, reverb send from 0.2 to 0.8, and a noise riser that climbs. Clear the old sweep on the Mid bass first.”
Underneath: lom_call(clip_slot, "duplicate_clip_to", ...) to copy the 4-bar loops into the second scene — the copy brings the automation with it, so lesson 3’s sweep arrives in the build clip — then clip_envelope_clear on that copy, which leaves lesson 3’s own clip and its checkpoint intact. A Session clip is lengthened by setting both end_marker and loop_end; the notes do not stretch, so the progression is written out three more times. Then clip_envelope_insert_step on three different parameters across two clips: a device knob, a mixer send, and the riser’s filter. Drift’s parameters are all 0–1, switches included, and Noise Gain starts at 0 — silence, not a neutral middle — which is why min/max get read before anything is written.
Lesson inside the lesson: Live evaluates a step edge as the end of what came before. Beat 0 returns the parameter’s own pre-envelope value; beat 4, the edge between the first step and the second, returns the first step’s value. Sample inside a step — beats 2, 6, 10, 14 — or correct automation reads as broken. An envelope belongs to a clip, not a track, which is why the same pad filter can sweep 0.35→0.95 in the loop and 0.2→1.0 in the build.
Checkpoint: clip_envelope_get on each of the three parameters returns a rising series — pad filter [0.2, 0.57, 1.0], reverb send [0.2, 0.46, 0.8], riser [0.15, 0.55, 1.0], sampled at beats 2, 30 and 62; the pad build clip is 64 beats with 64 notes; the noise clip is 64 beats long.
A.8 8. Arrangement — the 5:30 structure
Outcome: Session clips laid into the Arrangement: 32-bar intro, 16-bar build, 32-bar drop, 16-bar breakdown, 16-bar build, 32-bar drop, 24-bar outro — with named locators at each section.
The ask: “Lay this out as a track: intro 32 bars with drums and sub only, build 16, drop 32 with everything, breakdown 16 with pad and lead, build 16, drop 32, outro 24. Put a locator at each section.”
Underneath: arrangement_duplicate_clip once per tile — 152 calls for this arrangement, a loop over a section table, not typing — since a 4-bar loop tiles eight times across a 32-bar section. arrangement_create_clip is the other door: it builds a clip in the Arrangement from nothing, or from a file. arrangement_list_clips to verify; lom_call("live_set", "set_or_delete_cue") after moving the playhead (current_song_time) to each section start.
Lesson inside the lesson: a looped arrangement clip cannot be lengthened in place, and the mechanism is worth knowing: end_marker and loop_end do move, while end_time — where the clip actually stops on the timeline — does not move with them and has no setter. Resizing is a drag in Live’s GUI, so tiling is not a workaround, it is the operation that exists. Bar positions assume one time signature — Live’s API exposes no list of meter changes.
Checkpoint: arrangement_list_clips → Drums 38, Sub 32, Mid 16, Pad 28, Lead 20, Arp 16, Noise 2 — 152 clips; the drums run unbroken except bars 80–96, the breakdown. lom_count("live_set", "cue_points") → 7 at beats 0, 128, 192, 320, 384, 448, 576. The last clip ends at beat 672 — bar 168 — but lom_get("live_set", "song_length") reads 704: Live pads the timeline eight bars past the last clip, so song_length measures the timeline, not the music. Measure the music from the clips.
A.9 9. Audio — a vocal chop, warped
Outcome: an Audio track with a vocal phrase (a Core Library sample or your own), warped to 122, chopped to land on the breakdown.
The ask: “Bring in a vocal sample, warp it to the tempo, and place two chops in the breakdown on the off-beats.”
Underneath: browser_list("packs/Core Library/Samples/Loops/Tonal/Vocal") then browser_load onto an audio track — and a sample does not land on the track the way a device does, it lands in the highlighted clip slot. track_index only picks the track, so set lom_set("live_set view", "highlighted_clip_slot", {"__path__": "<slot>"}) first; unlike selected_device, that one has a setter. Then lom_get(clip, "warp_markers") returns opaque reprs — JSON cannot carry a WarpMarker — while each marker individually is a navigable path with beat_time and sample_time, so reading works and only writing needs clip_set_warp_markers(...), whose sample_time is in seconds, not frames. lom_set(clip, "warp_mode", 6) for Complex Pro, which is what a stretched vocal needs; duplicate_clip_to to cut a 2-beat chop in the next slot, then arrangement_duplicate_clip for the chops.
Lesson inside the lesson: clip_set_warp_markers is one of the few typed tools — it exists only because generic access genuinely cannot work there. Everything else about the audio clip is plain get/set.
Checkpoint: lom_get(clip, "warping") → true, warp_mode → 6; the map reads three markers, not the two that were written — beat 0, beat 16, and one at beat 16.03 that Live refuses to delete (remove_failed: 1), a 32nd past the end where nothing plays. Three clips on the audio track inside the breakdown: the 4-bar phrase at beat 320, chops at 338.5 and 354.5.
A.10 10. Mixing with a human in the room
Outcome: a balanced mix — levels, pans, sends, a glue compressor and limiter on the master — done with you, not for you.
The ask: “Watch the mixer. I’m going to set the drums and bass by ear; then balance everything else around them, and tell me every time I change something.”
Underneath: lom_observe on each track’s mixer_device volume value and on output_meter_level; you move faders by hand; Claude reads lom_poll_events, balances the rest with lom_set_batch, and reports back; lom_observers to list what is watched, lom_unobserve_all at the end (0 leaked). Measured: a fader move is 2 events, and four seconds of playback with eight output_meter_level observers is about 100 — Live coalesces meter updates rather than firing per frame, so the 2000-event buffer holds roughly a minute of it before dropped_events goes nonzero. browser_load("audio_effects/Glue Compressor") and a Limiter onto live_set master_track, selected first the way lesson 6 did.
Lesson inside the lesson: availability is per instance — MIDI tracks have no input_meter_left, only the main track has a crossfader; lom_describe reports these under unavailable. Observers are a pull with a ring buffer behind them; they catch what the user does in the GUI, which nothing else in the field reports. And the registry is shared across clients (issue #2): take a latest_seq baseline, never observe_clear what you did not create. Destructive calls — delete_*, remove_*, clear_*, crop — refuse to run without confirm=true; the Set is not in version control. Saving is yours: ⌘S.
Checkpoint: lom_observers → 0 active listeners after cleanup, 0 leaked; the main track has 3 devices — the Pro-Q from lesson 6 is still first in the chain, then the Glue Compressor and the Limiter. Every track’s volume is what you or Claude last set. One value that is not: the Pad’s A-Reverb send reads 0.2 rather than lesson 6’s 0.4, because lesson 7’s clip envelope drives that parameter and playback leaves it wherever the envelope ended. The arrangement plays from bar 1 to bar 168 with one intended gap — the breakdown, bars 80–96, where the drums drop out.
A.11 How the lessons are kept true
Each lesson’s Underneath and Checkpoint are run against a live Set when the lesson is written, the way the tutorial was. A lesson that cannot be checked with a query is not finished. When Live changes a member these lessons name, lom_describe is the source of truth and the lesson is wrong — fix the lesson.