from sideman import Live
live = Live()
[live.get(f"live_set tracks {i}", "name")["value"] for i in range(6)]['Drums', 'Sub', 'Mid', 'Pad', 'Lead', 'Arp']
Everything so far loops forever at the same intensity. A build is the opposite: sixteen bars that are not the same at the end as at the start. This lesson makes a second set of clips — the build versions — with the pad filter opening, the reverb send rising, and a noise riser climbing underneath.
Prerequisites: lesson 6 executed — six tracks, effects on the Lead, Pro-Q on the main track.
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.”
from sideman import Live
live = Live()
[live.get(f"live_set tracks {i}", "name")["value"] for i in range(6)]['Drums', 'Sub', 'Mid', 'Pad', 'Lead', 'Arp']
The four-bar loops in clip_slots 0 are the drop. The build goes in clip_slots 1 — same tracks, different clips — so lesson 8 can lay out either one without editing notes again. duplicate_clip_to takes a slot, which is a Live object, so it arrives as a __path__ marker.
for i in (0, 1, 2, 3):
live.call(f"live_set tracks {i} clip_slots 0", "duplicate_clip_to",
[{"__path__": f"live_set tracks {i} clip_slots 1"}])
[(live.get(f"live_set tracks {i}", "name")["value"],
live.get(f"live_set tracks {i} clip_slots 1", "has_clip")["value"])
for i in range(6)][('Drums', True),
('Sub', True),
('Mid', True),
('Pad', True),
('Lead', False),
('Arp', False)]
The duplicate is a real copy, automation included: the Mid bass’s filter sweep from lesson 3 is in the new clip too. In the build the mid bass should sit still, so that sweep goes — and clearing it here leaves lesson 3’s clip untouched, which is why the build got its own copy rather than an edit in place.
MID_BUILD = "live_set tracks 2 clip_slots 1 clip"
LP_MID = live.search(query="LP Freq", root="live_set tracks 2",
type="DeviceParameter")["results"][0]["path"]
live.envelope_get(MID_BUILD, LP_MID, times=[2.0, 6.0, 10.0, 14.0])["points"][{'time': 2.0, 'value': 0.3499999940395355},
{'time': 6.0, 'value': 0.5},
{'time': 10.0, 'value': 0.7199999690055847},
{'time': 14.0, 'value': 0.949999988079071}]
live.envelope_clear(MID_BUILD, LP_MID)
live.envelope_get(MID_BUILD, LP_MID, samples=3){'path': 'live_set tracks 2 clip_slots 1 clip',
'parameter': 'live_set tracks 2 devices 0 parameters 1',
'exists': False,
'points': []}
exists: false — the envelope is gone, not flattened to zero. Passing no parameter at all clears every envelope on the clip, which is rarely what you want and is why the parameter argument is worth typing.
A build is 64 beats. A Session clip can be lengthened in place by moving its end and loop markers — both, or the clip plays four bars and loops.
PAD_BUILD = "live_set tracks 3 clip_slots 1 clip"
live.set(PAD_BUILD, "end_marker", 64.0)
live.set(PAD_BUILD, "loop_end", 64.0)
live.get(PAD_BUILD, "length")["value"]64.0
The notes did not stretch — the clip is now 64 beats holding 4 bars of chords and 12 bars of silence. The progression repeats four times, so write the other three rounds at a bar offset.
chords = [(0.0, [53, 56, 60, 63]),
(4.0, [53, 56, 61, 65]),
(8.0, [51, 56, 60, 63]),
(12.0, [51, 55, 58, 63])]
voice_vel = [66, 62, 64, 72]
live.notes_add(PAD_BUILD, [
{"pitch": p, "start_time": start + round_ * 16, "duration": 3.5,
"velocity": voice_vel[i]}
for round_ in (1, 2, 3)
for start, pitches in chords
for i, p in enumerate(pitches)]){'path': 'live_set tracks 3 clip_slots 1 clip', 'added': 48}
live.notes_get(PAD_BUILD)["count"]64
A noise riser is one sustained note through a noise source with the filter opening. Drift can be that source: switch both oscillators off, the noise on, and hold the envelope at full sustain so the note does not decay over sixteen bars.
live.call("live_set", "create_midi_track", [-1])
live.set_batch([
{"path": "live_set tracks 6", "property": "name", "value": "Noise"},
{"path": "live_set tracks 6", "property": "color_index", "value": 2},
])
live.browser_load("instruments/Drift", track_index=6){'loaded': 'instruments/Drift', 'track': 'Noise'}
A bare Drift, not a preset: there is nothing to un-design. Its parameters come back as a paged vector, and their position is the only handle a path has on them — so build the name-to-index map once and never count by hand.
DEV = "live_set tracks 6 devices 0"
params = {p["name"]: i for i, p in
enumerate(live.get(DEV, "parameters", limit=100)["value"]["items"])}
def P(name):
return f"{DEV} parameters {params[name]}"
{n: params[n] for n in ("LP Freq", "Osc 1 On", "Osc 2 On", "Noise On",
"Noise Gain", "Env 1 Sustain", "Env 1 Attack")}{'LP Freq': 1,
'Osc 1 On': 31,
'Osc 2 On': 32,
'Noise On': 34,
'Noise Gain': 33,
'Env 1 Sustain': 38,
'Env 1 Attack': 35}
Read the ranges before writing them. Every one of these is 0–1, the switches included, and Noise Gain sits at 0 — silence, not a neutral setting. That is the whole argument for reading min/max instead of assuming a scale.
wanted = ["Osc 1 On", "Osc 2 On", "Noise On", "Noise Flt On", "Noise Gain",
"Env 1 Attack", "Env 1 Sustain", "LP Freq"]
ranges = {}
for r in live.get_batch([{"path": P(n), "property": prop}
for n in wanted for prop in ("value", "min", "max")])["results"]:
ranges.setdefault(r["path"], {})[r["property"]] = round(r["value"], 3)
[(n, ranges[P(n)]) for n in wanted][('Osc 1 On', {'value': 1.0, 'min': 0.0, 'max': 1.0}),
('Osc 2 On', {'value': 1.0, 'min': 0.0, 'max': 1.0}),
('Noise On', {'value': 1.0, 'min': 0.0, 'max': 1.0}),
('Noise Flt On', {'value': 1.0, 'min': 0.0, 'max': 1.0}),
('Noise Gain', {'value': 0.0, 'min': 0.0, 'max': 1.0}),
('Env 1 Attack', {'value': 0.111, 'min': 0.0, 'max': 1.0}),
('Env 1 Sustain', {'value': 0.7, 'min': 0.0, 'max': 1.0}),
('LP Freq', {'value': 1.0, 'min': 0.0, 'max': 1.0})]
live.set_batch([
{"path": P("Osc 1 On"), "property": "value", "value": 0.0},
{"path": P("Osc 2 On"), "property": "value", "value": 0.0},
{"path": P("Noise On"), "property": "value", "value": 1.0},
{"path": P("Noise Flt On"), "property": "value", "value": 1.0},
{"path": P("Noise Gain"), "property": "value", "value": 0.85},
{"path": P("Env 1 Attack"), "property": "value", "value": 0.0},
{"path": P("Env 1 Sustain"), "property": "value", "value": 1.0},
])["applied"]7
One note, sixteen bars long. The pitch is irrelevant — noise has none — so it sits on C3 out of habit.
NOISE = "live_set tracks 6 clip_slots 1 clip"
live.call("live_set tracks 6 clip_slots 1", "create_clip", [64.0])
live.notes_add(NOISE, [{"pitch": 60, "start_time": 0.0, "duration": 64.0,
"velocity": 100}])
live.get(NOISE, "length")["value"]64.0
A step per bar for the pad’s filter, a step every two bars for the reverb send, a step per bar for the riser. All three are (clip, parameter) pairs; two of them point at parameters on other objects than the clip’s own track — the send lives on the mixer, the filter on a device.
LP_PAD = live.search(query="LP Freq", root="live_set tracks 3",
type="DeviceParameter")["results"][0]["path"]
for b in range(16):
live.envelope_insert_step(PAD_BUILD, LP_PAD, time=b * 4, length=4,
value=0.2 + 0.8 * b / 15)
live.envelope_get(PAD_BUILD, LP_PAD, times=[2.0, 30.0, 62.0])["points"][{'time': 2.0, 'value': 0.20000000298023224},
{'time': 30.0, 'value': 0.5733333826065063},
{'time': 62.0, 'value': 1.0}]
SEND_A = "live_set tracks 3 mixer_device sends 0"
for b in range(8):
live.envelope_insert_step(PAD_BUILD, SEND_A, time=b * 8, length=8,
value=0.2 + 0.6 * b / 7)
live.envelope_get(PAD_BUILD, SEND_A, times=[4.0, 28.0, 60.0])["points"][{'time': 4.0, 'value': 0.20000000298023224},
{'time': 28.0, 'value': 0.4571428596973419},
{'time': 60.0, 'value': 0.800000011920929}]
LP_NOISE = P("LP Freq")
for b in range(16):
live.envelope_insert_step(NOISE, LP_NOISE, time=b * 4, length=4,
value=0.15 + 0.85 * b / 15)
live.envelope_get(NOISE, LP_NOISE, times=[2.0, 30.0, 62.0])["points"][{'time': 2.0, 'value': 0.15000002086162567},
{'time': 30.0, 'value': 0.5466666221618652},
{'time': 62.0, 'value': 1.0}]
Sample an envelope exactly on a step edge and you get the value from before that step — Live evaluates the boundary that way. Every check above samples inside a step (beat 2 of a four-beat step, not beat 0), and that is not a detail: the obvious check, “read the envelope at bar 1”, reads the wrong thing and makes correct automation look broken.
An envelope belongs to a clip, not to a track. The same pad filter is still on 0.35–0.95 in the four-bar loop and climbs 0.2–1.0 in the build, because those are two clips with two envelopes on one parameter. Automation that follows the clip is the reason the build and the drop can live in the same Set.
def rising(clip, param, times):
vs = [p["value"] for p in live.envelope_get(clip, param, times=times)["points"]]
assert vs == sorted(vs) and vs[-1] > vs[0], (param, vs)
return [round(v, 2) for v in vs]
print("pad filter ", rising(PAD_BUILD, LP_PAD, [2.0, 30.0, 62.0]))
print("reverb send", rising(PAD_BUILD, SEND_A, [4.0, 28.0, 60.0]))
print("riser ", rising(NOISE, LP_NOISE, [2.0, 30.0, 62.0]))
assert live.get(NOISE, "length")["value"] == 64.0
assert live.get(PAD_BUILD, "length")["value"] == 64.0
assert live.notes_get(PAD_BUILD)["count"] == 64
assert live.envelope_get(MID_BUILD, LP_MID, samples=3)["exists"] is False
assert live.count("live_set", "tracks")["count"] == 7
print("lesson 7 ok — 16-bar build clips, three rising envelopes, riser on track 6")pad filter [0.2, 0.57, 1.0]
reverb send [0.2, 0.46, 0.8]
riser [0.15, 0.55, 1.0]
lesson 7 ok — 16-bar build clips, three rising envelopes, riser on track 6