16  5. Melody and arp — the hook

A plucked Lead playing a two-bar call and a two-bar answer over the chords, with one note from outside the scale, and an Arp track running each chord as rising 16ths. This is the lesson where the track gets something you can hum.

Prerequisites: lesson 4 executed — Drums, Sub, Mid, Pad at tracks 0–3.

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.”

from sideman import Live

live = Live()
live.get("live_set", "scale_intervals")["value"]
[0, 2, 3, 5, 7, 8, 10]

16.1 The scale comes from Live

Lesson 1 set the key; read it back rather than hardcoding F minor. The Set knows its own root and intervals, so the melody can be derived from them — which is what makes this a lesson rather than a transcription.

root = live.get("live_set", "root_note")["value"]          # 5 = F
intervals = live.get("live_set", "scale_intervals")["value"]

scale = sorted(root + 12 * octave + i
               for octave in range(3, 6) for i in intervals)
[p for p in scale if 60 <= p <= 79]
[60, 61, 63, 65, 67, 68, 70, 72, 73, 75]

Those are the F minor scale tones from C4 up. The lead lives in the top half of that: high enough to sit over the pad, low enough not to whistle.

16.2 Two tracks

Same shape as before: make them, name them before any preset lands on them, then load.

for _ in range(2):
    live.call("live_set", "create_midi_track", [-1])

live.set_batch([
    {"path": "live_set tracks 4", "property": "name", "value": "Lead"},
    {"path": "live_set tracks 4", "property": "color_index", "value": 60},
    {"path": "live_set tracks 5", "property": "name", "value": "Arp"},
    {"path": "live_set tracks 5", "property": "color_index", "value": 69},
])["applied"]
4
live.browser_load("instruments/Drift/Guitar & Plucked/Chirp Pluck.adv", track_index=4)
live.browser_load("instruments/Drift/Synth Rhythmic/Echo Pulse.adv", track_index=5)
{'loaded': 'instruments/Drift/Synth Rhythmic/Echo Pulse.adv', 'track': 'Arp'}

16.3 Call and answer

Bars 1–2 ask, bars 3–4 answer. The call climbs to C5 and settles on Ab4 over Fm7 and Db; the answer goes higher — Eb5 is the top of the phrase — and comes back down to F4, the tonic, on the last bar.

One note is not in the scale: B4 (71), between Bb4 and C5. In F minor the scale has Bb, not B; borrowing the note a semitone up and falling straight back to Bb is the blue note. One is a colour. Two is a different key.

def note(pitch, start, dur, vel):
    return {"pitch": pitch, "start_time": start, "duration": dur, "velocity": vel}

call = [note(65, 0.0, 0.50, 96),    # F4
        note(68, 0.5, 0.50, 88),    # Ab4
        note(72, 1.0, 1.00, 100),   # C5
        note(70, 2.5, 0.50, 84),    # Bb4
        note(68, 3.0, 1.00, 92),    # Ab4
        note(65, 4.0, 0.75, 90),    # F4   — over Db
        note(68, 5.0, 0.50, 86),    # Ab4
        note(73, 5.5, 1.50, 98),    # Db5
        note(70, 6.0, 0.50, 80),    # Bb4  — thinned out below
        note(72, 6.5, 0.25, 76),    # C5   — thinned out below
        note(72, 7.0, 1.00, 88)]    # C5

answer = [note(68, 8.0, 0.50, 94),   # Ab4  — over Ab
          note(72, 8.5, 0.50, 90),   # C5
          note(75, 9.0, 1.00, 102),  # Eb5  — top of the phrase
          note(72, 10.5, 0.50, 86),  # C5
          note(71, 11.0, 0.75, 90),  # B4   — the blue note
          note(70, 12.0, 1.00, 92),  # Bb4  — over Eb
          note(67, 13.5, 0.50, 84),  # G4
          note(65, 14.0, 2.00, 88)]  # F4   — home

LEAD = "live_set tracks 4 clip_slots 0 clip"
live.call("live_set tracks 4 clip_slots 0", "create_clip", [16.0])
live.notes_add(LEAD, call + answer)
{'path': 'live_set tracks 4 clip_slots 0 clip', 'added': 19}
Figure 16.1: Call and answer across the four bars. The shaded rows are pitches F minor does not contain; the outlined block is the blue note, which Live names B3 where the comment says B4.

16.4 Thin it

Nineteen notes is two too many: bar 2 is crowded where the Db5 is still ringing. remove_notes takes a pitch and time window, not a list of ids, so “everything in the second half of bar 2” is one call. The defaults remove everything in the clip — always pass a window.

live.notes_remove(LEAD, from_time=6.0, time_span=0.75)
{'path': 'live_set tracks 4 clip_slots 0 clip', 'removed': 2}
g = live.notes_get(LEAD)
g["count"], sorted((n["start_time"], n["pitch"]) for n in g["notes"])[:8]
(17,
 [(0.0, 65),
  (0.5, 68),
  (1.0, 72),
  (2.5, 70),
  (3.0, 68),
  (4.0, 65),
  (5.0, 68),
  (5.5, 73)])
Figure 16.2: The Lead clip in Live, its range reading F3 - Eb4.

Sorted on the way out, because notes_get returns notes grouped by pitch rather than in time order. Nothing guarantees an order Live did not promise.

16.5 The arp

Each bar’s chord, straight up: the pad’s four voices, then the same four an octave higher, as 16ths. Eight tones fills half a bar, so the run happens twice per chord — 16 notes a bar, 64 over the phrase.

Humanising is not randomness for its own sake. The accent falls on the beat and the rest sit back, with a small deterministic wobble so no two 16ths are identical. Seeded, because a lesson whose output changes every run is not checkable.

import random

random.seed(122)
chords = [[53, 56, 60, 63],    # Fm7
          [53, 56, 61, 65],    # Db
          [51, 56, 60, 63],    # Ab
          [51, 55, 58, 63]]    # Eb

arp = []
for b, pitches in enumerate(chords):
    tones = pitches + [p + 12 for p in pitches]
    for i in range(16):
        vel = (104 if i % 4 == 0 else 78) + random.randint(-6, 6)
        arp.append(note(tones[i % 8], b * 4 + i * 0.25, 0.22, vel))

ARP = "live_set tracks 5 clip_slots 0 clip"
live.call("live_set tracks 5 clip_slots 0", "create_clip", [16.0])
live.notes_add(ARP, arp)
{'path': 'live_set tracks 5 clip_slots 0 clip', 'added': 64}
first_bar = sorted(live.notes_get(ARP, time_span=4.0)["notes"],
                   key=lambda n: n["start_time"])
live.notes_get(ARP)["count"], [(n["pitch"], int(n["velocity"])) for n in first_bar[:8]]
(64,
 [(53, 106),
  (56, 75),
  (60, 81),
  (63, 76),
  (65, 99),
  (68, 83),
  (72, 77),
  (75, 84)])
Figure 16.3: The Arp clip beside it: sixteen notes a bar climbing each chord, the humanised velocities visible as uneven heights in the lane below.

16.6 Read the whole Set in one round trip

Before deciding what the mix needs, look at all of it. get_batch sends N reads down one connection and — this is the point — each result carries its own ok. The last spec below asks a MIDI track for an input meter it does not have; the other reads come back regardless.

specs = [{"path": f"live_set tracks {i}", "property": p}
         for i in range(6) for p in ("name", "color_index")]
specs += [{"path": f"live_set tracks {i} clip_slots 0 clip", "property": "length"}
          for i in range(6)]
specs += [{"path": "live_set tracks 0", "property": "input_meter_left"}]

batch = live.get_batch(specs)
batch["count"], batch["ok_count"]
(19, 18)
names = [r["value"] for r in batch["results"] if r["ok"] and r["property"] == "name"]
lengths = [r["value"] for r in batch["results"] if r["ok"] and r["property"] == "length"]
[(r["property"], r["error"]) for r in batch["results"] if not r["ok"]]
[('input_meter_left',
  "RuntimeError: 'input_meter_left' unavailable on this Track.Track: MIDI tracks have no 'input_meter_left' property!")]
names, lengths
(['Drums', 'Sub', 'Mid', 'Pad', 'Lead', 'Arp'],
 [16.0, 16.0, 16.0, 16.0, 16.0, 16.0])
The lesson inside the lesson

get_batch isolates failures. One unavailable property returns its own error next to five successful reads instead of taking them down with it — which matters most when you are reading a Set you did not build and do not yet know what is on it. set_batch is the opposite by default: it stops on the first error, because a half-applied mix is worse than a refused one.

The melody came from scale_intervals, not from a constant in the code. Ask the Set what key it is in and the same cell works when the user changes their mind.

16.7 Checkpoint

assert live.count("live_set", "tracks")["count"] == 6
assert [live.get(f"live_set tracks {i}", "name")["value"] for i in (4, 5)] == ["Lead", "Arp"]
assert live.notes_get(ARP)["count"] == 64
assert live.notes_get(LEAD)["count"] == 17
assert 71 in [n["pitch"] for n in live.notes_get(LEAD)["notes"]], "the blue note is gone"
print("lesson 5 ok — Lead 17 notes with the B4, Arp 64 notes over 4 bars")
lesson 5 ok — Lead 17 notes with the B4, Arp 64 notes over 4 bars