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:
+30
-13
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user