Files
david 8cd796e3dd Initial commit: Arduino Uno Q USB Audio Synth project
- 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)
2026-06-22 22:46:36 -07:00

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))