8cd796e3dd
- USB gadget (UAC1 + MIDI) with boot persistence - USB host mode with auto-detection - Pure Data synth patch with ALSA + UDP netreceive MIDI input - MIDI rawmidi-to-UDP bridge (midi-bridge.py) - LED matrix visualization (led-matrix-viz.py via arduino-router RPC) - MCU sketch for LED matrix control (Arduino_RouterBridge + ArduinoGraphics) - Conftest/OPA Rego policy validation suite for .pd files - gen_synth_pd.py: programmatic .pd file generator - System services: midi-bridge, led-matrix-viz, pd-synth-auto, usb-role-detect - Justfile with deploy, enable, diagnostic recipes - Diagnostic script (diag.sh)
162 lines
5.2 KiB
Python
162 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Parse a .pd file into JSON for Conftest/Rego validation."""
|
|
import json, sys, re
|
|
from pathlib import Path
|
|
|
|
def parse_pd(filepath):
|
|
with open(filepath) as f:
|
|
text = f.read()
|
|
|
|
lines = text.split("\n")
|
|
records = []
|
|
x_index = 0
|
|
errors = []
|
|
|
|
for i, raw in enumerate(lines):
|
|
line = raw.strip()
|
|
if not line or line == ";":
|
|
continue
|
|
if not line.startswith("#"):
|
|
errors.append(f"line {i+1}: expected # prefix")
|
|
continue
|
|
|
|
raw = line[1:].strip()
|
|
if not raw.endswith(";"):
|
|
errors.append(f"line {i+1}: missing terminating semicolon")
|
|
continue
|
|
body = raw[:-1].strip()
|
|
|
|
if line.startswith("#N"):
|
|
nbody = body[1:].strip() # remove 'N'
|
|
parts = nbody.split(None, 1)
|
|
if parts[0] == "canvas":
|
|
tokens = parts[1].split()
|
|
rec = {
|
|
"type": "canvas",
|
|
"x": safe_int(tokens[0]),
|
|
"y": safe_int(tokens[1]),
|
|
"width": safe_int(tokens[2]),
|
|
"height": safe_int(tokens[3]),
|
|
"name": tokens[4] if len(tokens) > 4 else "",
|
|
}
|
|
records.append(rec)
|
|
else:
|
|
records.append({"type": "new", "data": rest})
|
|
|
|
elif line.startswith("#A"):
|
|
values = [safe_float(v) for v in rest.split()]
|
|
records.append({"type": "array_data", "values": values})
|
|
|
|
elif line.startswith("#X connect"):
|
|
xbody = body[1:].strip()
|
|
elems = xbody.split()
|
|
if len(elems) >= 5:
|
|
rec = {
|
|
"type": "connect",
|
|
"x_index": x_index,
|
|
"src": safe_int(elems[1]),
|
|
"outlet": safe_int(elems[2]),
|
|
"dst": safe_int(elems[3]),
|
|
"inlet": safe_int(elems[4]),
|
|
}
|
|
records.append(rec)
|
|
# Connect records do NOT increment Pd's object index
|
|
|
|
elif line.startswith("#X obj"):
|
|
xbody = body[1:].strip()
|
|
# xbody = "obj 20 20 notein"
|
|
elem, x_str, y_str, rest = xbody.split(None, 3)
|
|
obj_parts = rest.split()
|
|
rec = {
|
|
"type": "obj",
|
|
"x_index": x_index,
|
|
"x": safe_int(x_str),
|
|
"y": safe_int(y_str),
|
|
"name": obj_parts[0] if obj_parts else "",
|
|
"args": obj_parts[1:],
|
|
}
|
|
records.append(rec)
|
|
x_index += 1
|
|
|
|
elif line.startswith("#X msg"):
|
|
xbody = body[1:].strip()
|
|
elem, x_str, y_str, content = xbody.split(None, 3)
|
|
records.append({
|
|
"type": "msg",
|
|
"x_index": x_index,
|
|
"x": safe_int(x_str),
|
|
"y": safe_int(y_str),
|
|
"content": content,
|
|
})
|
|
x_index += 1
|
|
|
|
elif line.startswith("#X floatatom"):
|
|
xbody = body[1:].strip()
|
|
elems = xbody.split()
|
|
rec = {"type": "floatatom", "x_index": x_index, "x": safe_int(elems[1]), "y": safe_int(elems[2])}
|
|
if len(elems) > 3: rec["width"] = safe_int(elems[3])
|
|
if len(elems) > 4: rec["min"] = safe_float(elems[4])
|
|
if len(elems) > 5: rec["max"] = safe_float(elems[5])
|
|
records.append(rec)
|
|
x_index += 1
|
|
|
|
elif line.startswith("#X symbolatom"):
|
|
records.append({"type": "symbolatom", "x_index": x_index})
|
|
x_index += 1
|
|
|
|
elif line.startswith("#X text"):
|
|
xbody = body[1:].strip()
|
|
records.append({"type": "text", "x_index": x_index, "content": xbody})
|
|
x_index += 1
|
|
|
|
elif line.startswith("#X restore"):
|
|
xbody = body[1:].strip()
|
|
records.append({"type": "restore", "x_index": x_index, "data": xbody})
|
|
x_index += 1
|
|
|
|
elif line.startswith("#X coords"):
|
|
xbody = body[1:].strip()
|
|
records.append({"type": "coords", "x_index": x_index, "data": xbody})
|
|
x_index += 1
|
|
|
|
elif line.startswith("#X array"):
|
|
xbody = body[1:].strip()
|
|
records.append({"type": "array", "x_index": x_index, "data": xbody})
|
|
x_index += 1
|
|
|
|
else:
|
|
errors.append(f"line {i+1}: unknown record type: {line[:60]}")
|
|
|
|
return {
|
|
"file": str(filepath),
|
|
"x_record_count": x_index,
|
|
"records": records,
|
|
"parse_errors": errors,
|
|
}
|
|
|
|
def safe_int(s):
|
|
try:
|
|
# Handle Pd special values like "-" for empty
|
|
if s == "-":
|
|
return 0
|
|
return int(s)
|
|
except (ValueError, IndexError):
|
|
return 0
|
|
|
|
def safe_float(s):
|
|
try:
|
|
if s == "-":
|
|
return 0.0
|
|
return float(s)
|
|
except (ValueError, IndexError):
|
|
return 0.0
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print("Usage: pd2json.py <file.pd> [file2.pd ...]")
|
|
sys.exit(1)
|
|
results = []
|
|
for arg in sys.argv[1:]:
|
|
results.append(parse_pd(arg))
|
|
print(json.dumps(results if len(results) > 1 else results[0], indent=2))
|