Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
07e931b46This file is pinned to the selected release. Download the original to open it in your design tool.
"""Minimal AppleMIDI (RTP-MIDI, RFC 6295) session initiator for testing a
Patternflow panel from a PC with no MIDI driver installed.
python rtpmidi_probe.py <device-ip> [port]
Opens a session, sends a scripted set of messages, and prints what the
device sends back. Pure UDP, standard library only.
"""
import socket, struct, sys, time, random, threading
DEV = sys.argv[1] if len(sys.argv) > 1 else "192.168.0.180"
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 5004
SSRC = random.getrandbits(32)
TOKEN = random.getrandbits(32)
NAME = b"pf-probe"
ctl = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dat = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ctl.bind(("", 0)); dat.bind(("", ctl.getsockname()[1] + 1))
ctl.settimeout(3); dat.settimeout(3)
def invite(sock, port):
pkt = struct.pack("!HHIII", 0xFFFF, 0x494E, 2, TOKEN, SSRC) + NAME + b"\0"
sock.sendto(pkt, (DEV, port))
data, addr = sock.recvfrom(1500)
sig, cmd, ver, tok, ssrc = struct.unpack("!HHIII", data[:16])
name = data[16:].split(b"\0")[0].decode(errors="replace")
ok = cmd == 0x4F4B and tok == TOKEN
print(f" invite {port}: {'OK' if ok else 'NO'} peer ssrc={ssrc:08x} name={name!r}")
return ssrc if ok else None
def now_ts():
return int(time.time() * 10000) & 0xFFFFFFFFFFFFFFFF
def clock_sync(peer):
pkt = struct.pack("!HHIB3xQQQ", 0xFFFF, 0x434B, SSRC, 0, now_ts(), 0, 0)
dat.sendto(pkt, (DEV, PORT + 1))
data, _ = dat.recvfrom(1500)
sig, cmd, ssrc, count = struct.unpack("!HHIB", data[:9])
ts1, ts2, ts3 = struct.unpack("!QQQ", data[12:36])
print(f" clock sync: count={count} ts2={ts2}")
pkt = struct.pack("!HHIB3xQQQ", 0xFFFF, 0x434B, SSRC, 2, ts1, ts2, now_ts())
dat.sendto(pkt, (DEV, PORT + 1))
seq = random.getrandbits(16)
def send_midi(*msgs):
"""msgs: list of byte sequences; sent as one RTP packet, zero deltas."""
global seq
body = b""
for i, m in enumerate(msgs):
if i > 0:
body += b"\0" # delta time 0 between commands
body += bytes(m)
hdr = bytes([0x80, 0xE1]) + struct.pack("!HII", seq & 0xFFFF, now_ts() & 0xFFFFFFFF, SSRC)
seq += 1
if len(body) < 16:
cs = bytes([len(body)])
else:
cs = struct.pack("!H", 0x8000 | len(body))
dat.sendto(hdr + cs + body, (DEV, PORT + 1))
received = []
def listener():
dat.settimeout(0.5)
while True:
try:
data, _ = dat.recvfrom(1500)
except socket.timeout:
continue
except OSError:
return
if data[:2] == b"\xff\xff":
cmd = data[2:4]
if cmd == b"CK":
# device-initiated sync: answer count 1
ssrc, count = struct.unpack("!IB", data[4:9])
ts1 = struct.unpack("!Q", data[12:20])[0]
if count == 0:
dat.sendto(struct.pack("!HHIB3xQQQ", 0xFFFF, 0x434B, SSRC, 1, ts1, now_ts(), 0), (DEV, PORT + 1))
continue
if len(data) < 13:
continue
flags = data[12]
if flags & 0x80:
ln = ((flags & 0x0F) << 8) | data[13]; off = 14
else:
ln = flags & 0x0F; off = 13
payload = data[off:off + ln]
received.append(payload)
print(f" <- MIDI {payload.hex(' ')}")
def ctl_listener():
ctl.settimeout(0.5)
while True:
try:
data, _ = ctl.recvfrom(1500)
except socket.timeout:
continue
except OSError:
return
if data[:4] == b"\xff\xffCK":
ssrc, count = struct.unpack("!IB", data[4:9])
ts1 = struct.unpack("!Q", data[12:20])[0]
if count == 0:
ctl.sendto(struct.pack("!HHIB3xQQQ", 0xFFFF, 0x434B, SSRC, 1, ts1, now_ts(), 0), (DEV, PORT))
def bye():
ctl.sendto(struct.pack("!HHIII", 0xFFFF, 0x4259, 2, TOKEN, SSRC), (DEV, PORT))
if __name__ == "__main__":
import json, urllib.request
def status():
with urllib.request.urlopen(f"http://{DEV}/api/status", timeout=8) as r:
return json.load(r)
print("session:")
peer = invite(ctl, PORT)
if peer is None:
sys.exit(1)
invite(dat, PORT + 1)
clock_sync(peer)
threading.Thread(target=listener, daemon=True).start()
threading.Thread(target=ctl_listener, daemon=True).start()
time.sleep(0.5)
st = status()
print("status:", {k: st[k] for k in ("knobs", "params", "paramActive", "active")}, st.get("midi"))
print("CC20=127 (knob1 absolute -> 1000)")
send_midi([0xB0, 20, 127]); time.sleep(0.6)
st = status(); print(" params", st["params"], "active", st["paramActive"])
print("CC21=0, CC22=64, CC23=100")
send_midi([0xB0, 21, 0], [0xB0, 22, 64], [0xB0, 23, 100]); time.sleep(0.6)
st = status(); print(" params", st["params"], "active", st["paramActive"])
k0 = st["knobs"][0]
print("CC24=67 (knob1 +3 detents)")
send_midi([0xB0, 24, 67]); time.sleep(0.6)
st = status(); print(" knobs", st["knobs"], "(was", k0, ")")
print("Program Change 2")
before = st["active"]
send_midi([0xC0, 2]); time.sleep(1.5)
st = status(); print(" active", repr(before), "->", repr(st["active"]))
print("Note on/off 60 (button 1 press)")
send_midi([0x90, 60, 127]); time.sleep(0.2); send_midi([0x80, 60, 0]); time.sleep(0.4)
st = status(); print(" midi", st.get("midi"))
print("outbound: select pattern 1 over HTTP, expect Program Change back")
received.clear()
urllib.request.urlopen(f"http://{DEV}/api/patterns/select?index=1", timeout=8).read()
time.sleep(1.5)
print(" received", [p.hex(' ') for p in received])
st = status(); print(" midi", st.get("midi"))
bye()
time.sleep(0.3)
st = status(); print("after BY:", st.get("midi"))