LIGHT TIMING: the hold saves the set; what is under a cue includes the stem's lag

- After moving cues, the hold clicks File > Save Live Set in Live (System Events; osascript has
  Accessibility on the Tracks Mac), only when the set open in Live is the one checked
- "Under the cue" now looks from 10 ms before to 25 ms after it: a cue on a hit whose stem starts
  10-15 ms after the grid read as a quiet spot (Name Above All Names 737.1.1 was moved by mistake
  and has been put back)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 10:50:05 -04:00
co-authored by Claude Opus 5.5
parent 625d424bda
commit 11ac8aa8e0
4 changed files with 54 additions and 5 deletions
+36 -2
View File
@@ -874,7 +874,8 @@ def look(path, sec, span):
return None
att = [attack(env, i) for i in range(len(env))]
best = max(near, key=lambda i: att[i])
at = max([att[i] for i in near if abs(t0 + i * FRAME) <= 0.010] or [0])
# what is under the cue: the stems' hits start up to ~15 ms after the grid, so look that far after it
at = max([att[i] for i in near if -0.010 <= t0 + i * FRAME <= 0.025] or [0])
# where that hit starts: the first frame that climbs halfway from the quiet before it to its peak
lo, hi = min(env[max(0, best - 8):best] or [0]), max(env[best:best + 4] or [0])
start = next((j for j in range(max(0, best - 8), best + 4) if env[j] >= lo + 0.5 * (hi - lo)), best)
@@ -1033,6 +1034,32 @@ print(json.dumps({"done": done}))
"""
SAVE_SCRIPT = r"""
import json, os, subprocess, time
P = json.loads(%r)
before = os.path.getmtime(P["set"])
r = subprocess.run(["/usr/bin/osascript", "-e", 'with timeout of 15 seconds\n'
'tell application "System Events" to click menu item "Save Live Set" of menu "File" of menu bar 1 of process "Live"\n'
'end timeout'], capture_output=True, text=True, timeout=30)
saved = False
for _ in range(40):
if os.path.getmtime(P["set"]) != before:
saved = True
break
time.sleep(0.25)
print(json.dumps({"saved": saved, "error": (r.stderr or "").strip()[-120:]}))
"""
def save_set(cfg, rc, path):
"""File > Save Live Set in Live on the Tracks Mac (System Events; osascript has Accessibility there,
2026-10-22), and True once the set file on disk is newer."""
r = run_remote(cfg, rc["tracks_host"], SAVE_SCRIPT % json.dumps({"set": path}), timeout=60)
if not r.get("saved"):
log.warning("Live did not save %s: %s", os.path.basename(path), r.get("error") or "the file did not change")
return bool(r.get("saved"))
def fix_light_timing(cfg, rc):
"""A hold: move every off cue onto its hit in the open set (AbletonOSC's status panel command)."""
light = LAST.get("light") or {}
@@ -1050,6 +1077,13 @@ def fix_light_timing(cfg, rc):
if moved:
MOVED.setdefault(light["key"], [])
MOVED[light["key"]] = sorted(set(MOVED[light["key"]]) | set(moved))
# Saved straight away, but only when the set open in Live is the one that was checked.
live, path = light.get("live_set"), light["key"][0]
if live and os.path.basename(live) == os.path.basename(path) and rc.get("light_autosave", True):
if save_set(cfg, rc, path):
log.info("FIX lighting cues: %s saved", os.path.basename(path))
else:
return "MOVED, SAVE BY HAND"
log.info("FIX lighting cues moved onto the hit in Live: %s%s", ", ".join(bar_of(b) for b in moved) or "none",
" (not found in the open set: %s)" % ", ".join(bar_of(b) for b, _, n in r["done"] if not n)
if any(not n for _, _, n in r["done"]) else "")
@@ -1750,7 +1784,7 @@ def look(cfg, now=None, data_dir=None, sources=None):
lines["light_timing"] = eval_light_timing(rc, al, hits)
if al.get("set"):
cues = cue_times(rc, al["summary"])
LAST["light"] = {"key": (al["set"], al.get("mtime")), "summary": al["summary"],
LAST["light"] = {"key": (al["set"], al.get("mtime")), "summary": al["summary"], "live_set": al.get("live_set"),
"off": [(c, dt) for c, dt in off_cues(rc, cues, hits)
if c[0] not in set(MOVED.get((al["set"], al.get("mtime"))) or [])]}
except Exception as e: # noqa: BLE001 - a stem it cannot read must not hide the rest