READY: LIGHT TIMING (flash and FX cues against the drum hits); auto-fix on for testing

- LIGHT TIMING: every blinder flash / FX cue is timed against the Drums (Live) stem under it
  (deactivated clips count) on the Tracks Mac; cues a strong hit is more than 25 ms away from are named
  with song and bar
- Settings Overview: "Auto-fix on for testing" for 15 min to 2 h, outside the hours and over a pause;
  the AUTO-FIX key says TESTING, and it goes back to the hours by itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 10:18:46 -04:00
co-authored by Claude Opus 5.5
parent f179df59f1
commit d4e4092a9d
9 changed files with 300 additions and 19 deletions
+51 -1
View File
@@ -434,6 +434,56 @@ class TestWavesTune(unittest.TestCase):
self.assertEqual(msgs(rd.eval_tune(RC, keyed(GOOD), ableton())), ["NO TUNE CLIPS"])
def with_cues(al=None):
"""Flashes over Can't Help But Praise's drums (beats 100-190 of the fixture, at 151 BPM there)."""
al = al or ableton()
al["summary"]["tracks"].append(track("25", "42-Drums (Live)", group="20", clips=((100, 190, None),)))
al["summary"]["cues"] = [[104, 30], [104, 36], [108, 31], [112, 35], [116, 30], [120, 99]]
al["summary"]["hit_src"] = [["25", 100, 190, "/Projects/NA/261018 NA Project/MultiTracks/Drums (Live).wav", False, 0.0]]
al["light_names"] = {"30": "Blinder Flash 1", "31": "Blinder Flash 2", "35": "Blinder FX 1", "36": "Blinder FX 2"}
return al
class TestLightTiming(unittest.TestCase):
def test_cue_times_in_the_stem(self):
cues = rd.cue_times(RC, with_cues()["summary"])
self.assertEqual([(c[0], c[1], c[2]) for c in cues], [(104, 30, "CAN'T HELP BUT PRAISE"), (108, 31, "CAN'T HELP BUT PRAISE"),
(112, 35, "CAN'T HELP BUT PRAISE"), (116, 30, "CAN'T HELP BUT PRAISE")])
tempo = with_cues()["summary"]["tempo"]
self.assertAlmostEqual(cues[0][4], rd.seconds_at(tempo, 104) - rd.seconds_at(tempo, 100))
self.assertAlmostEqual(cues[0][5], 0.45 * 60 / 151 / 4, places=4) # under half a 16th at 151 BPM
def test_off_cues_are_named(self):
# [hit - cue (s), the hit's strength, strength under the cue]
hits = [[0.005, 5000, 5000], # on time
[0.037, 5200, 2000], # 37 ms early, a strong hit, little under the cue
[-0.053, 900, 300], # off, but a weak hit for this song: left alone
[0.040, 5100, 4800]] # off, but the cue already sits on a hit as strong: left alone
line = rd.eval_light_timing(RC, with_cues(), hits)
self.assertEqual(msgs(line), ["CUE OFF 28.1.1"])
self.assertEqual(line.as_dict()["details"], ["CAN'T HELP BUT PRAISE bar 28.1.1: Blinder Flash 2 is 37 ms before the hit"])
self.assertEqual(line.info, "3 CUES ON TIME")
def test_several_off(self):
hits = [[-0.04, 5000, 0], [0.037, 5200, 2000], [0.0, 5000, 5000], [0.0, 5000, 5000]]
line = rd.eval_light_timing(RC, with_cues(), hits)
self.assertEqual(msgs(line), ["2 CUES OFF"])
self.assertEqual(len(line.as_dict()["details"]), 2)
self.assertIn("Blinder Flash 1 is 40 ms after the hit", line.as_dict()["details"][0])
def test_a_stem_it_cannot_read(self):
line = rd.eval_light_timing(RC, with_cues(), ["unknown format: 3"] * 4)
self.assertEqual(msgs(line), [])
self.assertEqual(line.as_dict()["details"], ["could not read Drums (Live).wav"])
def test_bars_as_live_shows_them(self):
self.assertEqual(rd.bar_of(1315.75), "329.4.4")
self.assertEqual(rd.bar_of(0), "1.1.1")
def test_the_listener_runs(self):
compile(rd.HIT_SCRIPT % json.dumps({"cues": [["/x.wav", 1.5, 0.05]]}), "remote", "exec")
class TestTimecode(unittest.TestCase):
def test_seconds_through_the_tempo_map(self):
tempo = ableton()["summary"]["tempo"]
@@ -567,7 +617,7 @@ class TestLook(unittest.TestCase):
cfg["ready"].pop(k)
cfg["ready"]["prop_url"] = ""
r = rd.look(cfg, now=WEDNESDAY, sources=self.sources())
self.assertEqual(sorted(r["items"]), ["ears", "keys", "lights", "mics", "mix_foh", "routing", "timecode", "tracks", "tune"]) # LTC: tracks vs PCO
self.assertEqual(sorted(r["items"]), ["ears", "keys", "light_timing", "lights", "mics", "mix_foh", "routing", "timecode", "tracks", "tune"]) # LTC: tracks vs PCO
engine, snap = sp.Engine(cfg, None), sp.Snapshot()
snap.ready = r
tiles = engine.step(snap, WEDNESDAY)
+20
View File
@@ -2521,6 +2521,26 @@ class TestSwitches(unittest.TestCase):
self.assertTrue(any("Auto-fix is the AUTO-FIX key" in n for n in notes))
self.assertTrue(any("pause auto-fix" in n for n in notes))
def test_auto_fix_on_for_testing(self):
ran = []
self.engine.executor = lambda cfg, fix_id, chk: ran.append(fix_id) or True
self.snap.remote["tracks\0pgrep -xq Live"] = False # Ableton closed
thursday = SUNDAY - 3 * 86400 # outside the hours
run_steps(self.engine, self.snap, thursday, 25)
self.assertEqual(self.engine.auto_fix(self.snap, mock_comp(), thursday + 30), [])
sp.handle_commands("autofix test 30", self.engine, mock_comp(), self.snap, thursday + 31)
tiles = self.engine.step(self.snap, thursday + 32)
self.assertEqual(tiles["autofix"]["x"], "AUTO-FIX\nTESTING\nTIL 8:30AM")
self.engine.auto_fix(self.snap, mock_comp(), thursday + 40)
time.sleep(0.2)
self.assertEqual(ran, ["ableton"]) # fixed by itself on a Thursday
self.assertTrue(self.new_engine().autofix.test) # survives a restart
self.assertFalse(self.engine.autofix.on(thursday + 31 + 30 * 60)) # half an hour later: back to the hours
self.assertEqual(self.engine.step(self.snap, thursday + 31 + 30 * 60 + 1)["autofix"]["x"], "AUTO-FIX\nOFF\nON SUN 6:30AM")
sp.handle_commands("autofix test 30", self.engine, mock_comp(), self.snap, thursday + 4000)
sp.handle_commands("autofix resume", self.engine, mock_comp(), self.snap, thursday + 4060) # Stop testing
self.assertFalse(self.engine.autofix.on(thursday + 4061))
def test_a_hold_from_north_attleboro_flips_a_switch(self):
self.engine.run_fixes("autofix", self.snap, mock_comp(), SUNDAY)
self.assertTrue(self.engine.autofix.paused)