15  4. Harmony — the progression, in one undo

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}

15.1 The track, and two writes in one round trip

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.'}

15.2 A warm preset

“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'}

15.3 What is selected, as a stable path

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}

15.4 The progression

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}
Figure 15.1: The sixteen notes as four voicings, a bar each. No voice travels far between chords, and the whole progression sits inside Eb2 to F3 in Live’s naming.
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)])
Figure 15.2: The clip in Live, every block labelled with its own note name.

15.5 The pad in the mix, in one round trip

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.

The lesson inside the lesson

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.

15.6 Checkpoint

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