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)
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
#!/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))
|
||||
Reference in New Issue
Block a user