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:
+129
-6
@@ -90,8 +90,12 @@ class TestServices(unittest.TestCase):
|
||||
self.assertEqual([m for _, m, _ in line.faults], ["NO SET TUE 11/3"])
|
||||
both = {"261101 NA Project": {"exists": True}, "261103 NA First Tuesday Project": {"exists": True}}
|
||||
line = rd.eval_start(RC, svc, {"survey": both, "status": {"state": "done", "time": MONDAY, "results": [
|
||||
{"name": "261101 NA Project", "notes": ["TRANSPOSE THE LORD IS HERE E>B: CONSOLIDATE IN LIVE", "NEW SONG GRACE: BUILD IT BY HAND"]}]}}, MONDAY + 60)
|
||||
self.assertEqual([m for _, m, _ in line.faults], ["CONSOLIDATE THE LORD IS.", "NEW SONG GRACE"])
|
||||
{"name": "261101 NA Project", "notes": ["TRANSPOSE THE LORD IS HERE E>B", "NEW SONG GRACE: BUILD IT BY HAND"]}]}}, MONDAY + 60)
|
||||
self.assertEqual([m for _, m, _ in line.faults], ["NEW SONG GRACE"]) # transposed and consolidated
|
||||
left = {"261101 NA Project": {"exists": True, "transposed": {"The Lord Is Here": 22}},
|
||||
"261103 NA First Tuesday Project": {"exists": True, "transposed": {}}}
|
||||
line = rd.eval_start(RC, svc, {"survey": left, "status": None}, MONDAY)
|
||||
self.assertEqual([m for _, m, _ in line.faults], ["CONSOLIDATE THE LORD IS."]) # still transposed in the set
|
||||
line = rd.eval_start(RC, svc, {"survey": both, "status": None}, MONDAY)
|
||||
self.assertEqual((line.faults, line.info), ([], "SUN 11/1 TUE 11/3"))
|
||||
line = rd.eval_start(RC, svc, {"survey": {}, "status": {"state": "running", "time": MONDAY, "step": "COPYING 3/40"}}, MONDAY + 5)
|
||||
@@ -240,9 +244,15 @@ class TestBuild(unittest.TestCase):
|
||||
track("MidiTrack", 1, "2-MIDI", "0", midi_clip(1992, 2024, "INTRO") + midi_clip(2024, 2056, "VERSE 1")) +
|
||||
track("MidiTrack", 2, "3-MIDI", "0", midi_clip(1992, 2300, "LIGHTING CUES 47")) +
|
||||
track("AudioTrack", 3, "4-Guide [2026-01-01 000000]", "0", audio_clip(1992, 2300, "Guide", c("Guide [2026-01-01 000000].wav"),
|
||||
"Samples/Processed/Consolidate/Guide [2026-01-01 000000].wav", True)) +
|
||||
"Samples/Processed/Consolidate/Guide [2026-01-01 000000].wav", True)
|
||||
.replace('<Name Value="Guide" />', '<Name Value="Guide" /><Color Value="14" />')) +
|
||||
track("AudioTrack", 4, "5-Keys 1 [x]", "0", audio_clip(1992, 2368, "Keys 1", c("Keys [x].wav"), "Samples/Processed/Consolidate/Keys [x].wav",
|
||||
secs=313.3333333), ext))
|
||||
secs=313.3333333), ext) +
|
||||
track("AudioTrack", 5, "6-Drums (Live)", "0", audio_clip(1992, 2368, "Drums (Live)", c("Keys [x].wav"),
|
||||
"Samples/Processed/Consolidate/Keys [x].wav", secs=313.3333333)) +
|
||||
track("AudioTrack", 6, "7-Guide [2026-07-06 092943]", "0", audio_clip(1992, 2300, "Pink guide", c("Guide [2026-01-01 000000].wav"),
|
||||
"Samples/Processed/Consolidate/Guide [2026-01-01 000000].wav", True)
|
||||
.replace('<Name Value="Pink guide" />', '<Name Value="Pink guide" /><Color Value="28" />')))
|
||||
write_als(os.path.join(song, "GRACE.als"), live_set(song_tracks, [], 143))
|
||||
self.pads = os.path.join(d, "PADS")
|
||||
for k in ("E", "B", "D#"):
|
||||
@@ -257,6 +267,7 @@ class TestBuild(unittest.TestCase):
|
||||
"keep_groups": ["MIDI GROUP", "RESI TRACKS"], "pad_track": "AMBIENT PADS", "tune_track": "WAVES TUNE",
|
||||
"lighting_track": "LIGHTING CUES", "sections_track": "SECTIONS", "guide_track": "+GUIDE",
|
||||
"loopguide_track": "+LOOPGUIDE", "loop_cue_track": "Loop Cue", "tempo_track": "TEMPO", "send_a": 1.0,
|
||||
"consolidate": False,
|
||||
"services": [{"date": "261101", "folder": "NA", "name": "261101 NA Project", "label": "SUN 11/1",
|
||||
"songs": [{"title": "Grace", "key": "E", "bpm": 72}, {"title": "Grace", "key": "B", "bpm": 72},
|
||||
{"title": "Brand New", "key": "D", "bpm": 120}],
|
||||
@@ -302,10 +313,14 @@ class TestBuild(unittest.TestCase):
|
||||
c = A.clips(keys)[0]
|
||||
self.assertEqual((A.val(c, "IsWarped"), A.val(c, "WarpMode"), A.val(c, "ComplexProFormants"), A.val(c, "PitchCoarse")),
|
||||
("true", "6", "0", "-5"))
|
||||
drums = [t for t in A.family(ls, g2) if A.tname(t) == "6-Drums (Live)"][0]
|
||||
d = A.clips(drums)[0]
|
||||
self.assertEqual((A.val(d, "IsWarped"), A.val(d, "WarpMode"), A.val(d, "PitchCoarse")), ("false", "0", "0")) # unpitched: as it was
|
||||
self.assertEqual(sorted(x[1] for x in A.pending(root)), ["5-Keys 1 [x]"])
|
||||
self.assertEqual(A.val(keys, "DeviceChain/Mixer/Sends/TrackSendHolder/Send/Manual"), "1")
|
||||
self.assertEqual(A.val(keys, "DeviceChain/AudioOutputRouting/Target"), "AudioOut/External/S2")
|
||||
self.assertEqual([A.val(c, "Name") for c in A.clips(by["SECTIONS"])], ["CALL TO WORSHIP", "INTRO", "VERSE 1", "INTRO", "VERSE 1"])
|
||||
self.assertEqual(len(A.clips(by["+GUIDE"])), 2)
|
||||
self.assertEqual([A.val(x, "Name") for x in A.clips(by["+GUIDE"])], ["Guide", "Guide"]) # the red one, not the newer pink
|
||||
self.assertEqual(A.clips(by["+LOOPGUIDE"]), [])
|
||||
fr = c.find("SampleRef/FileRef")
|
||||
self.assertEqual(A.val(fr, "RelativePath"), "Samples/Imported/GRACE [E]/Keys [x].wav")
|
||||
@@ -315,7 +330,7 @@ class TestBuild(unittest.TestCase):
|
||||
self.assertEqual(tempo[0], (0.0, "72.00 BPM"))
|
||||
self.assertIn((tempo[-1][0], "72.00 BPM"), tempo)
|
||||
notes = st["results"][0]["notes"]
|
||||
self.assertIn("TRANSPOSE GRACE E>B: CONSOLIDATE IN LIVE", notes)
|
||||
self.assertIn("TRANSPOSE GRACE E>B", notes)
|
||||
self.assertIn("NEW SONG BRAND NEW: BUILD IT BY HAND", notes)
|
||||
self.assertIn("NO PAD IN D", notes)
|
||||
self.assertFalse([n for n in os.listdir(os.path.join(self.proj, "NA")) if n.endswith(".building")])
|
||||
@@ -375,3 +390,111 @@ class TestBuild(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class TestPitched(unittest.TestCase):
|
||||
def test_unpitched_stems(self):
|
||||
for n in ("22-Drums (Live)", "Aux Drums (Live)", "27-Perc", "PERC (LIVE) 2", "Percussion 1", "23-Shaker", "Shaker Loop_1",
|
||||
"Tambourine 1", "Sus Cymbals", "Toms", "Timbales", "Claps", "Clap FX_1", "Kick FX_1", "Snaps_1", "Impact FX",
|
||||
"Hit 1_1", "Boom FX", "Loop", "Loop 2", "Click Track", "119.00 BPM", "LAMB DRUM REF 91 BPM", "4-Guide [2026-01-01 000000]"):
|
||||
self.assertFalse(A.pitched(n), n)
|
||||
for n in ("5-Piano", "Sub Bass", "Synth Bass 2", "FX", "Sub Drop_1", "Riser FX", "Timpani", "Synth Loop", "Vox Loop",
|
||||
"Bells", "6-Piano [2026-07-13 095209]", "Choir", "AG 1"):
|
||||
self.assertTrue(A.pitched(n), n)
|
||||
|
||||
|
||||
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, []
|
||||
|
||||
def ask(self, addr, *args, wait=5.0):
|
||||
self.calls.append((addr,) + args)
|
||||
if addr == "/live/song/get/tempo":
|
||||
return [120.0]
|
||||
if addr == "/live/statuspanel/select_clip":
|
||||
self.selected = (args[0], round(args[2], 3))
|
||||
return list(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]
|
||||
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 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"
|
||||
return True
|
||||
|
||||
|
||||
class TestConsolidate(unittest.TestCase):
|
||||
def test_consolidates_pitched_and_puts_unpitched_back(self):
|
||||
d = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, d)
|
||||
path = os.path.join(d, "261101 NA Project", "261101 NA Project.als")
|
||||
clip = lambda n: audio_clip(1992, 2368, n, "/x.wav", warped=True).replace('<PitchCoarse Value="0" />', '<PitchCoarse Value="-5" />')
|
||||
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)])
|
||||
live = FakeLive(todo)
|
||||
A.STATUS = os.path.join(d, "status.json")
|
||||
status = A.Status({})
|
||||
log = []
|
||||
orig = A.live_set, A.save_live
|
||||
A.live_set, A.save_live = (lambda: path), (lambda p, lv: True)
|
||||
try:
|
||||
res = A.consolidate(path, {}, status, log, live)
|
||||
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.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)]
|
||||
live = FakeLive(todo)
|
||||
live.clips[(1, 1992.0)][0] = 0 # not there any more
|
||||
d = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, d)
|
||||
path = os.path.join(d, "s.als")
|
||||
write_als(path, live_set(track("AudioTrack", 2, "6-Piano", "-1",
|
||||
audio_clip(1992, 2368, "Piano", "/x.wav", True).replace('<PitchCoarse Value="0" />', '<PitchCoarse Value="2" />')), []))
|
||||
A.STATUS = os.path.join(d, "status.json")
|
||||
orig = A.live_set
|
||||
A.live_set = lambda: path
|
||||
try:
|
||||
res = A.consolidate(path, {}, A.Status({}), [], live)
|
||||
finally:
|
||||
A.live_set = orig
|
||||
self.assertEqual((res["consolidated"], res["left"]), (0, 1))
|
||||
self.assertNotIn(("Edit", "Consolidate"), live.calls)
|
||||
|
||||
|
||||
class TestHandBack(unittest.TestCase):
|
||||
def test_hands_abletonosc_back_once_after_start_used_live(self):
|
||||
sent = []
|
||||
orig = rd.run_remote, rd.hand_back
|
||||
rd.run_remote = lambda cfg, host, script, timeout=60: {"survey": {}, "status": {"state": "done", "live": 5.0}}
|
||||
rd.hand_back = lambda cfg, host: sent.append(host)
|
||||
rd.LAST.pop("start_handed", None)
|
||||
try:
|
||||
rd.start_look({}, RC, [])
|
||||
rd.start_look({}, RC, [])
|
||||
finally:
|
||||
rd.run_remote, rd.hand_back = orig
|
||||
self.assertEqual(sent, ["tracks"])
|
||||
rd.LAST["start_status"] = {"state": "running", "live": __import__("time").time()}
|
||||
self.assertTrue(rd.start_in_live())
|
||||
rd.LAST.pop("start_status")
|
||||
|
||||
Reference in New Issue
Block a user