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
+30 -13
View File
@@ -214,7 +214,9 @@ class TestBuild(unittest.TestCase):
p = lambda n: os.path.join(base, "Samples/Imported", n)
ext = ("Ext. Out", "5/6 Keys", "AudioOut/External/S2")
last_week = (
track("AudioTrack", 57, "TEMPO", clips=audio_clip(0, 1132, "143.00 BPM", "Samples/Recorded/x.aif", warped=True)) +
track("AudioTrack", 57, "TEMPO", clips=audio_clip(0, 1132, "143.00 BPM", "Samples/Recorded/x.aif", warped=True).replace(
"<WarpMarkers />", '<WarpMarkers><WarpMarker Id="0" SecTime="0" BeatTime="0" />'
'<WarpMarker Id="1" SecTime="0.013111888111888112" BeatTime="0.03125" /></WarpMarkers>')) +
track("AudioTrack", 58, "LTC", clips=audio_clip(0, 4000, "LTC", p("LTC.wav"), "Samples/Imported/LTC.wav", True)) +
track("MidiTrack", 59, "SECTIONS", clips=midi_clip(0, 196, "CALL TO WORSHIP") + midi_clip(196, 236, "INTRO")) +
track("GroupTrack", 60, "MIDI GROUP") +
@@ -328,6 +330,10 @@ class TestBuild(unittest.TestCase):
self.assertTrue(os.path.exists(os.path.join(os.path.dirname(path), "Samples/Imported/LTC.wav")))
tempo = [(A.span(c)[0], A.val(c, "Name")) for c in A.clips(by["TEMPO"])]
self.assertEqual(tempo[0], (0.0, "72.00 BPM"))
for c in A.clips(by["TEMPO"]): # a leader clip: its markers are the tempo Live plays
bpm = float(A.val(c, "Name").split()[0])
for m in c.findall("WarpMarkers/WarpMarker"):
self.assertAlmostEqual(float(m.get("SecTime")), float(m.get("BeatTime")) * 60 / bpm)
self.assertIn((tempo[-1][0], "72.00 BPM"), tempo)
notes = st["results"][0]["notes"]
self.assertIn("TRANSPOSE GRACE E>B", notes)
@@ -407,9 +413,8 @@ class FakeLive:
"""Live as AbletonOSC and the menus show it: one clip per track, consolidated when Edit > Consolidate is clicked."""
def __init__(self, todo, open_set=True):
self.clips = {(i, b): [1, -5, 1, "/Samples/Imported/%s.wav" % n] for i, n, b, _ in todo}
self.names = {i: n for i, n, _, _ in todo}
self.selected, self.calls = None, []
self.clips = {(i, b): [1, -5, 1, "/Samples/Imported/%s.wav" % n] for i, n, b, _, _ in todo}
self.selected, self.frozen, self.calls = None, None, []
def ask(self, addr, *args, wait=5.0):
self.calls.append((addr,) + args)
@@ -418,20 +423,30 @@ class FakeLive:
if addr == "/live/statuspanel/select_clip":
self.selected = (args[0], round(args[2], 3))
return list(args) + [1]
if addr == "/live/statuspanel/drop_tails":
return list(args) + [1, args[1]]
if addr == "/live/statuspanel/set_clip":
c = self.clips[(args[0], round(args[2], 3))]
c[1], c[2] = args[3], args[4]
if len(args) > 5:
self.calls.append(("name", args[5]))
return list(args[:3]) + c[:3]
return None
def clip(self, i, name, beat):
return tuple(self.clips.get((i, round(beat, 3)), [0, 0, 0, ""]))
def enabled(self, item, menu="Edit"):
return item == "Flatten Track" and self.frozen is not None
def menu(self, item, menu="Edit"):
self.calls.append((menu, item))
if item == "Consolidate" and self.selected:
c = self.clips[self.selected]
c[1], c[3] = 0, "/Samples/Processed/Consolidate/x.wav"
if item == "Freeze Track" and self.selected:
self.frozen = self.selected[0]
if item == "Flatten Track":
for (i, b), c in self.clips.items():
if i == self.frozen:
c[1], c[3] = 0, "/Samples/Processed/Freeze/Freeze x.wav"
return True
@@ -444,7 +459,7 @@ class TestConsolidate(unittest.TestCase):
write_als(path, live_set(track("GroupTrack", 1, "FULL SURRENDER") + track("AudioTrack", 2, "6-Piano", "1", clip("Piano")) +
track("AudioTrack", 3, "26-Drums", "1", clip("Drums")) + track("AudioTrack", 4, "7-Bass", "1"), []))
todo = A.pending(A.load(path))
self.assertEqual(todo, [(1, "6-Piano", 1992.0, True), (2, "26-Drums", 1992.0, False)])
self.assertEqual(todo, [(1, "6-Piano", 1992.0, True, "Piano"), (2, "26-Drums", 1992.0, False, "Drums")])
live = FakeLive(todo)
A.STATUS = os.path.join(d, "status.json")
status = A.Status({})
@@ -456,14 +471,16 @@ class TestConsolidate(unittest.TestCase):
finally:
A.live_set, A.save_live = orig
self.assertEqual((res["consolidated"], res["reset"], res["left"]), (1, 1, 0))
self.assertEqual(live.clips[(1, 1992.0)], [1, 0, 0, "/Samples/Processed/Consolidate/x.wav"]) # consolidated, warp off
self.assertEqual(live.clips[(2, 1992.0)][:3], [1, 0, 0]) # drums: own key, not consolidated
self.assertEqual([c for c in live.calls if c[0] == "Edit"], [("Edit", "Consolidate")])
self.assertEqual(live.clips[(1, 1992.0)], [1, 0, 0, "/Samples/Processed/Freeze/Freeze x.wav"]) # rendered, warp off
self.assertEqual(live.clips[(2, 1992.0)][:3], [1, 0, 0]) # drums: own key, not rendered
self.assertEqual([c for c in live.calls if c[0] == "Edit"], [("Edit", "Freeze Track"), ("Edit", "Flatten Track")])
self.assertTrue([c for c in live.calls if c[0] == "name" and c[1].startswith("Piano [20")]) # named as Consolidate names it
self.assertIn(("/live/statuspanel/drop_tails", 1, "6-Piano"), live.calls) # no Freeze tail left
self.assertIn("261101 NA PROJECT: 1 UNPITCHED CLIPS PUT BACK IN THEIR KEY", log)
self.assertTrue(status.data.get("live"))
def test_a_clip_changed_in_live_is_left(self):
todo = [(1, "6-Piano", 1992.0, True)]
todo = [(1, "6-Piano", 1992.0, True, "Piano")]
live = FakeLive(todo)
live.clips[(1, 1992.0)][0] = 0 # not there any more
d = tempfile.mkdtemp()
@@ -479,7 +496,7 @@ class TestConsolidate(unittest.TestCase):
finally:
A.live_set = orig
self.assertEqual((res["consolidated"], res["left"]), (0, 1))
self.assertNotIn(("Edit", "Consolidate"), live.calls)
self.assertNotIn(("Edit", "Freeze Track"), live.calls)
class TestHandBack(unittest.TestCase):