RESI VOX reads what each page 13 VOX AJA button really sends (now DQ-Rack 33-36) from Companion's store; a source no button sends is MIC WRONG

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 12:53:17 -04:00
co-authored by Claude Opus 5.5
parent a4d5fbe1aa
commit 12a9e506e1
3 changed files with 78 additions and 2 deletions
+1 -1
View File
@@ -165,7 +165,7 @@ everything on the page at once.
| LINEUP TV | ProdDash's Band Lineup shows this plan, nobody who is not on it or who declined, and everyone on the Band and Communication teams | re-syncs the board from Planning Center |
| TRACKS OUT | every location in `"locations"` (AB, SC, WS) has this weekend's project folder | copies NA's project to the locations that have none (as `261018 AB Project`, without `Backup/`). It is copied under a temporary name and renamed at the end, so a half-finished copy never looks sent. It never overwrites a folder that is already there and never runs by itself. |
| PROP OUT | a `.probundle` is in this weekend's folder of the Chronicles share (`/Volumes/Location Assets/261025`, read on the ProPresenter Mac). **SHARE NOT MOUNTED**: the share itself is not mounted there (2026-10-22: it had dropped off and the key said NOT IN 261025 although the bundle was there). The ProP Mac keeps it mounted by itself: a LaunchAgent (`org.waterschurch.keep-mounted.location-assets`, from `remote-access/mac-keep-mounted.sh`) looks every minute and at login, mounts it again with the Chronicles login saved in the Keychain, and unmounts and remounts it when it hangs after sleep or a network drop (log: `~/Library/Logs/keep-mounted-location-assets.log` on the ProP Mac). | starts that agent now and waits for the share (about 6 s), then looks again. Also an auto-fix. |
| RESI VOX | the Resi song (PCO description says RESI, else the last song before the message) goes to the locations with its lead singer's mic: the lead from the description ("Genesis lead"), their VOX from the lineup screen ("[VOX 4] MIX 4"; PCO team positions if the lineup has none), against what the Resi AJA's `5 - Lead Vocal` channel (21) really receives, read from the card with Dante's own query (UDP 4440, read only). **RESI V2 NOT VOX 4** (red). Two leads ("Mike and DJ lead"): the song's Audio note in PCO says whose mic goes out ("V3" or a name); without one it is flagged, never switched: **2 LEADS: MIKE DJ**. Also only flagged: **JOSH NOT ON LINEUP**, **LEAD ON VOX 5: NO BUTTON**. The lineup is what counts: a PCO Audio note naming another VOX is only mentioned in the details. | presses that VOX's AJA button (Companion page 13, row 3, col 1-4: AJA ch 21 <- BOH-AD4Q-A ch n), then reads the AJA back. Also an auto-fix (any time, never while a deck records: the READY page is not looked at then). |
| RESI VOX | the Resi song (PCO description says RESI, else the last song before the message) goes to the locations with its lead singer's mic: the lead from the description ("Genesis lead"), their VOX from the lineup screen ("[VOX 4] MIX 4"; PCO team positions if the lineup has none), against what the Resi AJA's `5 - Lead Vocal` channel (21) really receives, read from the card with Dante's own query (UDP 4440, read only). Which Dante channel is "VOX n" is read from Companion's page 13 VOX n AJA button itself (today the DQ-Rack's 33-36 `DQ-VOX 1..4`), so changing those buttons needs no settings change; anything no button sends is **RESI MIC WRONG**. **RESI V2 NOT VOX 4** (red). Two leads ("Mike and DJ lead"): the song's Audio note in PCO says whose mic goes out ("V3" or a name); without one it is flagged, never switched: **2 LEADS: MIKE DJ**. Also only flagged: **JOSH NOT ON LINEUP**, **LEAD ON VOX 5: NO BUTTON**. The lineup is what counts: a PCO Audio note naming another VOX is only mentioned in the details. | presses that VOX's AJA button (Companion page 13, row 3, col 1-4: AJA ch 21 <- BOH-AD4Q-A ch n), then reads the AJA back. Also an auto-fix (any time, never while a deck records: the READY page is not looked at then). |
| START | every service Planning Center lists this week (this location's Sunday and every special service: First / Together Tuesday, Morning Worship, ...) has its Ableton set. **NO SET SUN 11/1** (red when due): hold START. While it builds the key shows what it is doing (**SAVING JEHOVAH**, **COPYING 40/120**). **CONSOLIDATE THE LORD IS**: the saved set still has clips of that song transposed and not consolidated (read from the set file, so it goes once the set is saved consolidated). After a build, for a day: **NEW SONG GRACE** (not in Finished Songs: build it by hand, page 3 of the workflow PDF). See *START* below. | builds the missing sets (on the Tracks Mac, in the background), then consolidates every transposed clip in Live (step 9 below). Never runs by itself. |
**Where the answers come from:**
+44 -1
View File
@@ -2409,12 +2409,55 @@ def companion_var(cfg, conn, name):
return r.read().decode("utf-8", "replace").strip()
def vox_sources(cfg, rv):
"""{VOX n: (Dante device, channel number)}: what each VOX AJA button on Companion really sends, read from
Companion's own store (2026-10-27 the buttons were moved from the AD4Q-A's 1-4 to the DQ-Rack's 33-36 and the
panel kept expecting the AD4Q-A). Empty when it cannot be read."""
import sqlite3
db = os.path.expanduser((cfg.get("companion") or {}).get("db") or "~/Library/Application Support/companion/v5.0/db.sqlite")
out = {}
try:
con = sqlite3.connect("file:%s?mode=ro" % urllib.parse.quote(db), uri=True, timeout=2)
try:
for n, (page, row, col) in (rv.get("buttons") or {}).items():
pg = con.execute("select value from pages where id=?", (int(page),)).fetchone()
cid = ((json.loads(pg[0]).get("controls") or {}).get(str(row)) or {}).get(str(col)) if pg else None
ctl = con.execute("select value from controls where id=?", (cid,)).fetchone() if cid else None
for step in (json.loads(ctl[0]).get("steps") or {}).values() if ctl else []:
for acts in (step.get("action_sets") or {}).values():
for a in acts or []:
o = a.get("options") or {}
dev = (o.get("sourceDevice") or {}).get("value")
if (o.get("destinationDevice") or {}).get("value") == rv["aja"] and dev:
ch = (o.get("sourceChannel_" + dev) or {}).get("value")
if ch:
out[int(n)] = (dev, int(ch))
finally:
con.close()
except (sqlite3.Error, ValueError, KeyError, TypeError, AttributeError):
return {}
return out
def resi_now(cfg, rv):
"""The VOX number the Resi AJA's lead channel receives now (None: something else), and what it is."""
ip = companion_var(cfg, rv.get("dante_conn", "audinate-dantecontroller"), rv["aja"] + "_ip")
conn = rv.get("dante_conn", "audinate-dantecontroller")
ip = companion_var(cfg, conn, rv["aja"] + "_ip")
got = dante_rx(ip, rv.get("channel", 21)) if ip and ip != "Not found" else None
if got is None:
raise ReadyError("AJA NO ANSWER")
every = vox_sources(cfg, rv)
srcs = {k: v for k, v in every.items() if v[0] == got["tx_device"]}
if every and not srcs:
return None, got # not what any VOX button sends: a hold presses the lead's
if srcs:
# The channel's number on its device: from the device's channel names, else the name's own number ("33 DQ-VOX 1").
try:
ch = json.loads(companion_var(cfg, conn, got["tx_device"] + "_tx_names")).index(got["tx_channel"]) + 1
except (ValueError, OSError):
m = re.match(r"^\s*(\d+)\b", got["tx_channel"] or "")
ch = int(m.group(1)) if m else None
return next((k for k, (_, c) in srcs.items() if c == ch), None), got
if got["tx_device"] != rv.get("mics", "BOH-AD4Q-A"):
return None, got
n = vox_of(got["tx_channel"])
+33
View File
@@ -986,6 +986,39 @@ class TestResiVox(unittest.TestCase):
self.assertEqual(self.run_eval("Genesis lead\nRESI TX", 2)[0], ["RESI V2 NOT VOX 4"])
self.assertEqual(self.run_eval("Genesis lead\nRESI TX", 2)[2], 4)
def test_the_buttons_say_where_each_vox_comes_from(self):
"""2026-10-27: the VOX AJA buttons now send the DQ-Rack's 33-36; the panel reads what each button sends."""
import sqlite3
d = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, d)
db = os.path.join(d, "db.sqlite")
con = sqlite3.connect(db)
con.execute("create table pages (id integer, value text)")
con.execute("create table controls (id text, value text)")
con.execute("insert into pages values (13, ?)", (json.dumps({"controls": {"3": {str(n): "bank:v%d" % n for n in range(1, 5)}}}),))
for n in range(1, 5):
act = {"definitionId": "makeCrosspointDropDown", "options": {
"destinationDevice": {"value": "STUDIO-A-Resi-AJA-SDI"}, "sourceDevice": {"value": "BOH-DQ-Rack"},
"sourceChannel_BOH-DQ-Rack": {"value": 32 + n}, "sourceChannel_BOH-AD4Q-A": {"value": n}}}
con.execute("insert into controls values (?, ?)", ("bank:v%d" % n, json.dumps({"steps": {"0": {"action_sets": {"down": [act]}}}})))
con.commit()
con.close()
cfg = {"companion": {"db": db}}
self.assertEqual(rd.vox_sources(cfg, self.RV), {n: ("BOH-DQ-Rack", 32 + n) for n in range(1, 5)})
names = ["%02d DQ-CH" % i for i in range(1, 33)] + ["33 DQ-VOX 1", "34 DQ-VOX 2", "35 DQ-VOX 3", "36 DQ-VOX 4"]
got = {"tx_channel": "33 DQ-VOX 1", "tx_device": "BOH-DQ-Rack", "name": "5 - Lead Vocal"}
orig = rd.companion_var, rd.dante_rx
rd.companion_var = lambda cfg, conn, name: json.dumps(names) if name.endswith("_tx_names") else "169.254.54.105"
rd.dante_rx = lambda ip, ch: dict(got)
try:
self.assertEqual(rd.resi_now(cfg, self.RV)[0], 1)
got["tx_channel"] = "36 DQ-VOX 4"
self.assertEqual(rd.resi_now(cfg, self.RV)[0], 4)
got.update(tx_channel="01-VOX 1", tx_device="BOH-AD4Q-A") # the old source: not a button's now
self.assertIsNone(rd.resi_now(cfg, self.RV)[0])
finally:
rd.companion_var, rd.dante_rx = orig
def test_no_resi_mark_means_the_last_song_before_the_message(self):
self.assertEqual(rd.resi_song(self.plan("Rosaliz lead"))["title"], "Name Above All Names")
self.assertEqual(self.run_eval("Rosaliz lead", 2), ([], "VOX 2 ROSALIZ", 2))