MIDI drops show in about 1-2 s
- NA Tracks and the ProPresenter Mac: one long-lived ssh each streams the MIDI sessions (checked twice a second, sent on change) instead of an ssh login every 5 s; a silent stream gives no verdict after 12 s. - NA Ingest's own session is read twice a second (was every 2 s). - MIDI links show a drop after 1 s (was 3 s); everything redraws twice a second (was once). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+60
-4
@@ -146,6 +146,7 @@ class Snapshot:
|
||||
self.vars = {} # (label, name) -> str or None
|
||||
self.midi = {} # session name -> {"peers": [...]}
|
||||
self.remote_midi = {} # host -> the same, read on that Mac over SSH (None: unreachable)
|
||||
self.remote_midi_time = {} # host -> when it last reported
|
||||
self.peer_addr = {} # (host or "", session, peer) -> address it was last seen at
|
||||
self.conn_hosts = {} # Companion connection label -> its "host" setting
|
||||
self.midi_time = 0.0
|
||||
@@ -430,6 +431,8 @@ def check_midi_peer(chk, snap, ctx):
|
||||
# "host": a session on another Mac (e.g. NA Tracks -> Lighting PC, the FX lighting cues),
|
||||
# read over SSH. Unreachable or not read yet = no verdict (that Mac's own ping says if it is down).
|
||||
sessions = snap.remote_midi.get(chk["host"]) if chk.get("host") else (snap.midi if snap.midi_time else None)
|
||||
if chk.get("host") and ctx["now"] - snap.remote_midi_time.get(chk["host"], ctx["now"]) > 12:
|
||||
sessions = None # that Mac stopped reporting: no verdict
|
||||
if sessions is None:
|
||||
return [("peer", Result(OK))]
|
||||
sess = sessions.get(chk["session"])
|
||||
@@ -837,7 +840,9 @@ class Engine:
|
||||
deb = self.debounce if debounce is None else debounce
|
||||
for key, (tile_id, res, chk) in raw.items():
|
||||
tr = self.trackers.setdefault(key, Tracker())
|
||||
event = tr.update(res, now, 0 if chk["type"] == "network" else chk.get("debounce_seconds", deb),
|
||||
# A MIDI peer leaving a session is a clean event: show it after 1 s, not the general 3 s.
|
||||
event = tr.update(res, now, 0 if chk["type"] == "network" else
|
||||
chk.get("debounce_seconds", 1 if chk["type"] == "midi_peer" else deb),
|
||||
self.flap_window)
|
||||
if event == "fault":
|
||||
self.events.append(("FAULT", "%s %s" % (self.tiles[tile_id]["label"], res.msg)))
|
||||
@@ -1580,6 +1585,57 @@ def parse_remote_midi(out):
|
||||
return None
|
||||
|
||||
|
||||
def remote_midi_stream_script(interval=0.5):
|
||||
"""Like remote_midi_script, but keeps running: prints the sessions whenever they change (and every
|
||||
5 s anyway), checking twice a second. It ends by itself when the ssh connection goes."""
|
||||
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "coremidi.py"), encoding="utf-8").read()
|
||||
return ("import json, time\nns = {'__name__': 'spcm'}\nexec(compile(%r, 'coremidi.py', 'exec'), ns)\n"
|
||||
"last, sent = None, 0.0\n"
|
||||
"while True:\n"
|
||||
" s = json.dumps(ns['sessions']())\n"
|
||||
" if s != last or time.time() - sent > 5:\n"
|
||||
" print('SPMIDI' + s, flush=True)\n"
|
||||
" last, sent = s, time.time()\n"
|
||||
" time.sleep(%s)\n" % (src, interval))
|
||||
|
||||
|
||||
def stream_remote_midi(snap, cfg, host, stop):
|
||||
"""One long-lived ssh per Mac (NA Tracks, ProPresenter) reporting its MIDI sessions as they
|
||||
change, so a dropped link shows in about a second instead of up to 5 s + an ssh login."""
|
||||
script = remote_midi_stream_script()
|
||||
while not stop.is_set():
|
||||
proc = None
|
||||
try:
|
||||
proc = subprocess.Popen(ssh_cmd(cfg, host)[:1] + ["-o", "ServerAliveInterval=2", "-o", "ServerAliveCountMax=3"]
|
||||
+ ssh_cmd(cfg, host)[1:] + ["/usr/bin/python3", "-u", "-"],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
||||
proc.stdin.write(script)
|
||||
proc.stdin.close()
|
||||
for line in proc.stdout:
|
||||
data = parse_remote_midi(line)
|
||||
if data is not None:
|
||||
snap.remote_midi[host], snap.remote_midi_time[host] = data, time.time()
|
||||
remember_peers(snap, host, data)
|
||||
if stop.is_set():
|
||||
break
|
||||
except (OSError, ValueError) as e:
|
||||
log.warning("MIDI stream from %s: %s", host, e)
|
||||
finally:
|
||||
if proc and proc.poll() is None:
|
||||
proc.terminate()
|
||||
snap.remote_midi[host] = None # no verdict until it reports again
|
||||
stop.wait(3)
|
||||
|
||||
|
||||
def start_remote_midi_streams(snap, cfg, stop):
|
||||
threads = []
|
||||
for host in remote_midi_hosts(cfg):
|
||||
t = threading.Thread(target=stream_remote_midi, args=(snap, cfg, host, stop), name="midi-" + host, daemon=True)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
return threads
|
||||
|
||||
|
||||
def poll_remote_midi(snap, cfg):
|
||||
script = remote_midi_script()
|
||||
for host in remote_midi_hosts(cfg):
|
||||
@@ -2004,16 +2060,16 @@ def start_pollers(cfg, snap, stop, data_dir=None):
|
||||
Periodic("slow-variables", 3.0, lambda: poll_variables(c3, snap, slow), stop),
|
||||
Periodic("conn-hosts", 60.0, lambda: poll_conn_hosts(snap, cfg), stop),
|
||||
Periodic("pings", 3.0, lambda: poll_pings(snap, ping_hosts(cfg, snap), iface), stop),
|
||||
Periodic("midi", 2.0, lambda: poll_midi(snap, cfg), stop),
|
||||
Periodic("midi", 0.5, lambda: poll_midi(snap, cfg), stop),
|
||||
Periodic("system", 10.0, lambda: poll_system(snap, cfg), stop),
|
||||
Periodic("bonjour", 15.0, lambda: poll_mdns(snap), stop),
|
||||
Periodic("http", 5.0, lambda: poll_http(snap, http_checks(cfg)), stop),
|
||||
Periodic("remote", 10.0, lambda: poll_remote(snap, cfg), stop),
|
||||
Periodic("remote-midi", 5.0, lambda: poll_remote_midi(snap, cfg), stop),
|
||||
Periodic("dante-map", 30.0, lambda: poll_dante_map(snap, cfg, c4, dante_map_path(data_dir)), stop),
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
threads += start_remote_midi_streams(snap, cfg, stop)
|
||||
if any(c["type"] == "artnet" for t in cfg["tiles"] for c in t.get("checks", [])):
|
||||
start_artnet_listener(snap, stop)
|
||||
return threads
|
||||
@@ -2085,7 +2141,7 @@ def run(cfg, data_dir, dry_run=False, stop=None):
|
||||
comp.set_custom(names["command"], "")
|
||||
handle_commands(cmd, engine, comp, snap, now)
|
||||
last_eval = 0.0 # redraw now, so a press shows at once
|
||||
if now - last_eval >= 1.0:
|
||||
if now - last_eval >= 0.5: # redraw twice a second
|
||||
last_eval = now
|
||||
tiles = engine.step(snap, now)
|
||||
if not dry_run:
|
||||
|
||||
Reference in New Issue
Block a user