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:
@@ -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
@@ -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"])
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user