START: bright red guide, no transposing unpitched stems, consolidate + warp off in Live

Per the Location Setup worksheet (2026-10-26):
- a song saved twice holds two guides; START takes the bright red one (Live clip color 14), not the newer light pink one
- drums, percussion, aux drums and other unpitched stems are never transposed (by stem name; sub bass and FX are)
- every transposed clip is consolidated in Live (Edit > Consolidate) and its warp turned off, then the set is saved;
  a hold of START does it for this week's existing sets too. New AbletonOSC commands: clip, select_clip, set_clip
- the START key's CONSOLIDATE comes from the saved set (clips still transposed), not the last build's log

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 14:12:38 -04:00
co-authored by Claude Opus 5.5
parent 1f98af4bab
commit e191150f7a
6 changed files with 561 additions and 37 deletions
+78 -11
View File
@@ -1848,7 +1848,11 @@ def look(cfg, now=None, data_dir=None, sources=None):
lines["track_bundle"] = eval_track_bundle(rc, al)
except Exception as e: # noqa: BLE001
failed(["tracks", "mix_foh", "lights", "keys", "tune", "light_timing"] + (["track_bundle"] if rc.get("locations") else []), e)
if rc.get("tracks_host") and rc.get("ableton_osc"):
if rc.get("tracks_host") and rc.get("ableton_osc") and start_in_live():
for k in ("routing", "ears"):
lines[k] = Line()
lines[k].info = "START IN LIVE"
elif rc.get("tracks_host") and rc.get("ableton_osc"):
try:
lines["routing"], lines["ears"], _ = mix_plan(rc, src["live"]())
except Exception as e: # noqa: BLE001
@@ -1938,13 +1942,53 @@ SERVICES = {} # the services START builds, kept a quarter of an
IS_LIVE = [lambda: False] # the Poller's "a service is on": the hold copies gigabytes, never during one
START_SURVEY = r'''
import json, os
import gzip, json, os, re
import xml.etree.ElementTree as ET
P = json.loads(%r)
out = {}
CACHE = os.path.expanduser("~/Library/Caches/status-panel-start-pending.json")
try:
cache = json.load(open(CACHE))
except Exception:
cache = {}
def transposed(path):
# {song group: clips still transposed} (consolidated ones are back at 0), by the set's mtime
key = "%%s|%%s" %% (path, os.path.getmtime(path))
if key not in cache:
ls = ET.parse(gzip.open(path)).getroot().find("LiveSet")
up = {}
for t in ls.find("Tracks"):
g = t.find("TrackGroupId")
up[t.get("Id")] = (t.find("Name/EffectiveName").get("Value"), g.get("Value") if g is not None else "-1")
def top(i):
while up.get(i, ("", "-1"))[1] in up:
i = up[i][1]
return up.get(i, ("", ""))[0]
got = {}
for t in ls.find("Tracks"):
for c in t.iter("AudioClip"):
if c.find("PitchCoarse") is not None and float(c.find("PitchCoarse").get("Value")) != 0:
song = top(t.get("Id"))
got[song] = got.get(song, 0) + 1
cache[key] = got
return cache[key]
out, keep = {}, {}
for s in P["services"]:
d = os.path.join(os.path.expanduser(P["projects"]), s["folder"], s["name"])
out[s["name"]] = {"exists": os.path.isdir(d) and any(n.endswith(".als") for n in os.listdir(d)),
"building": os.path.isdir(d + ".building")}
als = [os.path.join(d, n) for n in os.listdir(d) if n.endswith(".als") and not n.startswith(".")] if os.path.isdir(d) else []
out[s["name"]] = {"exists": bool(als), "building": os.path.isdir(d + ".building")}
if als:
path = max(als, key=os.path.getmtime)
try:
out[s["name"]]["transposed"] = transposed(path)
k = "%%s|%%s" %% (path, os.path.getmtime(path))
keep[k] = cache[k]
except Exception:
pass
try:
with open(CACHE, "w") as f:
json.dump(keep, f)
except OSError:
pass
try:
st = json.load(open(os.path.expanduser("~/Library/Caches/status-panel-start.json")))
except Exception:
@@ -2092,7 +2136,9 @@ def start_params(rc, services, mode):
"lighting_track": rc.get("lighting_track", "LIGHTING CUES"), "sections_track": rc.get("sections_track", "SECTIONS"),
"guide_track": rc.get("guide_track", "+GUIDE"), "loopguide_track": rc.get("loopguide_track", "+LOOPGUIDE"),
"loop_cue_track": rc.get("loop_cue_track", "Loop Cue"), "tempo_track": rc.get("tempo_track", "TEMPO"),
"send_a": 1.0, "save_songs": rc.get("start_save_songs", True), "services": services}
"send_a": 1.0, "save_songs": rc.get("start_save_songs", True), "services": services,
"guide_color": rc.get("start_guide_color", 14), "unpitched": rc.get("start_unpitched") or None,
"consolidate": rc.get("start_consolidate", True)}
def start_script(params):
@@ -2103,8 +2149,21 @@ def start_script(params):
def start_look(cfg, rc, services):
return run_remote(cfg, rc["tracks_host"], START_SURVEY % json.dumps({"projects": rc.get("projects", ""), "services": [
look = run_remote(cfg, rc["tracks_host"], START_SURVEY % json.dumps({"projects": rc.get("projects", ""), "services": [
{"folder": x["folder"], "name": x["name"]} for x in services]}), timeout=30)
st = look.get("status") or {}
LAST["start_status"] = st
# START used AbletonOSC from the Tracks Mac (consolidating): hand it back to Companion once it is done.
if st.get("live") and st.get("state") != "running" and LAST.get("start_handed") != st["live"]:
hand_back(cfg, rc["tracks_host"])
LAST["start_handed"] = st["live"]
return look
def start_in_live():
"""START is consolidating in Live just now (it holds AbletonOSC's answers on the Tracks Mac)."""
st = LAST.get("start_status") or {}
return st.get("state") == "running" and time.time() - (st.get("live") or 0) < 600
def eval_start(rc, services, look, now):
@@ -2128,14 +2187,22 @@ def eval_start(rc, services, look, now):
if res.get("name") not in names:
continue
for note in res.get("notes") or []:
m = re.match(r"TRANSPOSE (.+?) \S+>\S+", note)
n = re.match(r"NEW SONG (.+?):", note)
if m:
line.fault(WARN, "CONSOLIDATE " + short(m.group(1), 12), "%s: %s" % (res["name"], note))
elif n:
if n:
line.fault(WARN, "NEW SONG " + short(n.group(1), 12), "%s: %s" % (res["name"], note))
else:
line.notes.append("%s: %s" % (res["name"], note))
if not running:
# The workflow consolidates every transposed clip (and turns its warp off): a hold does it in Live.
for svc in services:
for song, n in sorted(((survey.get(svc["name"]) or {}).get("transposed") or {}).items()):
line.fault(WARN, "CONSOLIDATE " + short(song.upper(), 12),
"%s: %s has %d transposed clip%s not consolidated: hold START to consolidate them in Live"
% (svc["name"], song, n, "" if n == 1 else "s"))
if st.get("state") == "done" and now - (st.get("time") or 0) < 86400:
for note in st.get("log") or []:
if "CONSOLIDATE IN LIVE:" in note or note.startswith("OPEN ") or "NOT CONSOLIDATED" in note or "DID NOT SAVE" in note:
line.notes.append(note)
line.info = " ".join(x["label"] for x in services) or "NO SERVICES"
return line