START: render transposed clips with Freeze/Flatten, TEMPO leader clips at their own tempo

Tested in Live on copies of 261101 (2026-10-26):
- Edit > Consolidate cannot be aimed at one clip from a script (no LOM selection; Edit > Select Loop
  selects every track, so Consolidate merged the SECTIONS markers and lighting cues). Each pitched track is
  now Freeze Track + Flatten Track (same audio: the stems have no devices), warp off, named like Consolidate
  names it; the Freeze tail clips and "(Freeze)" track names are removed. Groups unfolded to select, refolded.
- The TEMPO label clips lead the song tempo (IsSongTempoMaster): START renamed the base set's 143 clip
  without its warp markers, so Live played 261101 at 143 and consolidating rendered songs at the wrong
  length. lead_tempo() makes the markers match the BPM.
- AbletonOSC commands match Live's numbered/stamped track names; new refold and drop_tails commands.
- A run that stopped picks up flattened clips; START's look runs before the ROUTING/IN EARS reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 15:26:07 -04:00
co-authored by Claude Opus 5.5
parent a3b38cbd5d
commit 47a2c12a12
5 changed files with 216 additions and 64 deletions
+68 -7
View File
@@ -6,15 +6,25 @@
Answers (<track name>, <beat>, <pitch>, <moved: 0 or 1>). Undo in Live (Cmd+Z) puts it back.
START's consolidating (ableton_start.py) finds an arrangement clip by <track index> (Live's, no returns),
<track name> (any track of that name when the index has moved) and the <beat> it starts at:
<track name> (any track of that name when the index has moved) and the <beat> it starts at. A track with no name of
its own shows its number in Live ("56-Piano" is "5-Piano" in the set file) and its clip's name ("Piano [2026-10-26
113835]" after a consolidate, "Piano (Freeze)" after a flatten), so only the name in between is compared:
/live/statuspanel/clip <index> <name> <beat>
Answers (<index>, <name>, <beat>, <found>, <pitch_coarse>, <warping>, <file path>).
/live/statuspanel/select_clip <index> <name> <beat>
Selects the clip in the arrangement (Edit > Consolidate then works on it). Answers (..., <found>).
/live/statuspanel/set_clip <index> <name> <beat> <pitch_coarse> <warping>
Sets the clip's transposition, then its warp. Answers (..., <found>, <pitch_coarse>, <warping>).
Selects the clip in the arrangement (Edit > Consolidate then works on it), unfolding the groups it is in (Live
cannot select a track inside a folded group). Answers (..., <found>).
/live/statuspanel/refold
Folds the groups select_clip unfolded back up. Answers (<groups folded>).
/live/statuspanel/drop_tails <index> <name>
Deletes the "... (Freeze tail)" clips Flatten Track leaves after a clip, and takes "(Freeze)" out of the track's
name (back to Live's own name). Answers (<index>, <name>, <deleted>, <track name>); deleted is -1 when this
Live cannot delete a clip from a script.
/live/statuspanel/set_clip <index> <name> <beat> <pitch_coarse> <warping> [<new clip name>]
Sets the clip's transposition, then its warp (and its name). Answers (..., <found>, <pitch_coarse>, <warping>).
"""
import logging
import re
import Live
@@ -46,12 +56,18 @@ def register(handler):
handler.osc_server.add_handler("/live/statuspanel/move_note", move_note)
def bare(n):
# "56-Piano [2026-10-26 113835]" and "56-Piano (Freeze)" are the file's "5-Piano"
n = re.sub(r"^\s*\d+\s*-\s*", "", str(n))
n = re.sub(r"\[.*?\]|\(freeze\)", " ", n, flags=re.I)
return " ".join(n.split()).upper()
def find(params):
song = handler.song
i, name, beat = int(params[0]), str(params[1]).strip().upper(), float(params[2])
i, name, beat = int(params[0]), bare(params[1]), float(params[2])
ts = list(song.tracks)
order = ([ts[i]] if 0 <= i < len(ts) and ts[i].name.strip().upper() == name else []) + \
[t for t in ts if t.name.strip().upper() == name]
order = ([ts[i]] if 0 <= i < len(ts) and bare(ts[i].name) == name else []) + \
[t for t in ts if bare(t.name) == name]
for t in order:
try:
arr = t.arrangement_clips
@@ -71,10 +87,18 @@ def register(handler):
_, c = find(params)
return (int(params[0]), str(params[1]), float(params[2])) + info(c)
unfolded = []
def select_clip(params):
t, c = find(params)
if c is not None:
song = handler.song
g = t.group_track if t.is_grouped else None
while g is not None:
if g.fold_state:
g.fold_state = 0
unfolded.append(g)
g = g.group_track if g.is_grouped else None
view = Live.Application.get_application().view
view.show_view("Arranger")
view.focus_view("Arranger")
@@ -84,14 +108,51 @@ def register(handler):
logger.info("statuspanel: select %s at %s: %s", params[1], params[2], c is not None)
return int(params[0]), str(params[1]), float(params[2]), int(c is not None)
def refold(params):
n = 0
while unfolded:
g = unfolded.pop()
try:
g.fold_state = 1
n += 1
except Exception:
pass
return (n,)
def set_clip(params):
_, c = find(params)
if c is not None:
c.pitch_coarse = int(params[3])
c.warping = bool(int(params[4]))
if len(params) > 5 and str(params[5]):
c.name = str(params[5])
logger.info("statuspanel: %s at %s: pitch %s warp %s", params[1], params[2], params[3], params[4])
return (int(params[0]), str(params[1]), float(params[2])) + info(c)[:3]
handler.osc_server.add_handler("/live/statuspanel/clip", clip)
handler.osc_server.add_handler("/live/statuspanel/select_clip", select_clip)
handler.osc_server.add_handler("/live/statuspanel/set_clip", set_clip)
handler.osc_server.add_handler("/live/statuspanel/refold", refold)
def drop_tails(params):
i, name = int(params[0]), bare(params[1])
ts = list(handler.song.tracks)
order = ([ts[i]] if 0 <= i < len(ts) and bare(ts[i].name) == name else []) + [t for t in ts if bare(t.name) == name]
if not order:
return i, str(params[1]), 0, ""
t = order[0]
n = -1
if hasattr(t, "delete_clip"):
n = 0
for c in list(t.arrangement_clips):
if c.name.endswith("(Freeze tail)"):
t.delete_clip(c)
n += 1
if "(Freeze)" in t.name:
t.name = "" # Live's own name again, from the renamed clip
if "(Freeze)" in t.name:
t.name = re.sub(r"\s*\(Freeze\)", "", t.name)
logger.info("statuspanel: %s: %d freeze tails deleted, now %s", params[1], n, t.name)
return i, str(params[1]), n, str(t.name)
handler.osc_server.add_handler("/live/statuspanel/drop_tails", drop_tails)