Files
uno-q-audio-synth/pd-validator/pd.rego
T
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

272 lines
8.2 KiB
Rego

package main
# ─── Pd .pd file format validator (OPA v1.x syntax) ───
# Based on puredata.info/docs/developer/PdFileFormat
# OPA version: 1.15.2 (bundled with conftest 0.68.2)
# Input structure (from pd2json parser):
# {
# "x_record_count": N,
# "records": [{ "type": "obj"|"connect"|..., "x_index": N, ... }],
# "parse_errors": [...]
# }
# Get a record by its x_index
record_by_idx(idx) := r if {
some r_rec in input.records
r_rec.x_index == idx
r := r_rec
}
xcount := input.x_record_count
# Check if an object is a signal (~) type
is_signal(name) if {
endswith(name, "~")
}
# ═══════════════════════════════════════════════════════
# VIOLATIONS
# ═══════════════════════════════════════════════════════
# Conftest evaluates rules named `violation`, `violations`, `deny`, `warn`
# Out-of-bounds source index
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.src >= xcount
msg := sprintf("connect[%d]: src index %d out of range (max %d)", [rec.x_index, rec.src, xcount - 1])
}
# Out-of-bounds destination index
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.dst >= xcount
msg := sprintf("connect[%d]: dst index %d out of range (max %d)", [rec.x_index, rec.dst, xcount - 1])
}
# Negative source
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.src < 0
msg := sprintf("connect[%d]: negative src index %d", [rec.x_index, rec.src])
}
# Negative destination
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.dst < 0
msg := sprintf("connect[%d]: negative dst index %d", [rec.x_index, rec.dst])
}
# Negative outlet
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.outlet < 0
msg := sprintf("connect[%d]: negative outlet %d", [rec.x_index, rec.outlet])
}
# Negative inlet
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.inlet < 0
msg := sprintf("connect[%d]: negative inlet %d", [rec.x_index, rec.inlet])
}
# Self-loop
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.src == rec.dst
msg := sprintf("connect[%d]: self-loop (src=%d dst=%d)", [rec.x_index, rec.src, rec.dst])
}
# Duplicate connections
violation contains msg if {
some a, b in input.records
a.type == "connect"
b.type == "connect"
a.x_index < b.x_index
a.src == b.src
a.outlet == b.outlet
a.dst == b.dst
a.inlet == b.inlet
msg := sprintf("connect[%d]/[%d]: duplicate (%d,%d)->(%d,%d)", [a.x_index, b.x_index, a.src, a.outlet, a.dst, a.inlet])
}
# Signal-to-control connection
violation contains msg if {
some rec in input.records
rec.type == "connect"
src_rec := record_by_idx(rec.src)
dst_rec := record_by_idx(rec.dst)
src_rec.type == "obj"
dst_rec.type == "obj"
is_signal(src_rec.name)
not is_signal(dst_rec.name)
msg := sprintf("connect[%d]: signal (~) outlet %d (%s) to control inlet %d (%s)", [rec.x_index, rec.src, src_rec.name, rec.dst, dst_rec.name])
}
# No canvas definition
violation contains msg if {
not has_canvas
msg := "No #N canvas definition found"
}
has_canvas if {
some rec in input.records
rec.type == "canvas"
}
# Outlet exceeds known count (skip -1 = variable)
violation contains msg if {
some rec in input.records
rec.type == "connect"
src_rec := record_by_idx(rec.src)
src_rec.type == "obj"
known_outlets[src_rec.name]
known_outlets[src_rec.name] != -1
rec.outlet >= known_outlets[src_rec.name]
msg := sprintf("connect[%d]: obj %d (%s) has %d outlets, referenced outlet %d", [rec.x_index, rec.src, src_rec.name, known_outlets[src_rec.name], rec.outlet])
}
# Inlet exceeds known count (skip -1 = variable)
violation contains msg if {
some rec in input.records
rec.type == "connect"
dst_rec := record_by_idx(rec.dst)
dst_rec.type == "obj"
known_inlets[dst_rec.name]
known_inlets[dst_rec.name] != -1
rec.inlet >= known_inlets[dst_rec.name]
msg := sprintf("connect[%d]: obj %d (%s) has %d inlets, referenced inlet %d", [rec.x_index, rec.dst, dst_rec.name, known_inlets[dst_rec.name], rec.inlet])
}
# ═══════════════════════════════════════════════════════
# KNOWLEDGE BASE: inlet/outlet counts by object name
# ═══════════════════════════════════════════════════════
# -1 means variable/unknown
known_inlets := {
# Audio signal objects
"osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 0,
"*~": 2, "+~": 2, "-~": 2, "/~": 2, "pow~": 2,
"lop~": 2, "hip~": 2, "bp~": 3, "vcf~": 3,
"cos~": 1, "wrap~": 1, "abs~": 1,
"clip~": 3, "env~": 4,
"dbtopower~": 1, "powertodb~": 1,
"mtof": 1, "ftom": 1,
"dac~": 2, "adc~": 0,
"sig~": 1, "snapshot~": 1,
"line~": 2, "vline~": 2,
"tabread4~": 2, "tabwrite~": 2, "tabread~": 2,
"send~": 1, "receive~": 1,
"delwrite~": 2, "delread~": 1, "vd~": 2,
"rfft~": 1, "rifft~": 1,
"inlet~": 0, "outlet~": 0,
"block~": 0, "switch~": 1,
# MIDI
"notein": 0, "noteout": 1,
"ctlin": 0, "ctlout": 1,
"midiin": 0, "midiout": 1,
# Data flow
"pack": -1, "unpack": 1,
"makenote": 2, "stripnote": 2,
"route": 1, "select": 1, "spigot": 2,
"trigger": 1, "t": 1,
"float": 1, "f": 1, "symbol": 1, "int": 1,
# Math
"+": 2, "-": 2, "*": 2, "/": 2, "pow": 2,
"div": 2, "mod": 2,
"max": 2, "min": 2,
"moses": 2, "until": 1,
"timer": 1, "random": 1,
# Data
"array": 1, "tabread": 1, "tabwrite": 2,
"text": 1, "textfile": 1,
"value": 0, "v": 0,
"soundfiler": 1,
# GUI
"openpanel": 0, "savepanel": 0,
"cnv": 0, "hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1,
"bng": 0, "tgl": 0, "nbx": 1, "vu": 0,
"print": 1,
# Time
"pipe": 1, "delay": 1, "del": 1,
"metro": 1, "tempo": 1,
"line": 2, "poly": 2,
# Send/receive
"s": 1, "send": 1, "r": 0, "receive": 0,
"netsend": 2, "netreceive": 1,
# Misc
"loadbang": 0, "loadb": 0, "bang": 0,
"inlet": 0, "outlet": 0,
"expr": 1, "clone": 1,
}
# Conftest checks deny, warn, and violation (singular)
known_outlets := {
# Audio signal
"osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 1,
"*~": 1, "+~": 1, "-~": 1, "/~": 1, "pow~": 1,
"lop~": 1, "hip~": 1, "bp~": 1, "vcf~": 1,
"cos~": 1, "wrap~": 1, "abs~": 1,
"clip~": 1, "env~": 1,
"dbtopower~": 1, "powertodb~": 1,
"mtof": 1, "ftom": 1,
"dac~": 0, "adc~": 2,
"sig~": 1, "snapshot~": 1,
"line~": 1, "vline~": 1,
"tabread4~": 1, "tabwrite~": 0, "tabread~": 1,
"send~": 0, "receive~": 1,
"delwrite~": 0, "delread~": 1, "vd~": 1,
"rfft~": 2, "rifft~": 1,
"inlet~": 1, "outlet~": 0,
"block~": 0, "switch~": 0,
# MIDI
"notein": 2, "noteout": 0,
"ctlin": 1, "ctlout": 0,
"midiin": 1, "midiout": 0,
# Data flow
"pack": 1, "unpack": -1,
"makenote": 0, "stripnote": 2,
"route": -1, "select": 1, "spigot": 2,
"trigger": -1, "t": -1,
"float": 1, "f": 1, "symbol": 1, "int": 1,
# Math
"+": 1, "-": 1, "*": 1, "/": 1,
"div": 1, "mod": 1, "pow": 1,
"max": 1, "min": 1,
"moses": 2, "until": 1,
"timer": 2, "random": 1,
# Data
"array": 2, "tabread": 1, "tabwrite": 0,
"text": 2, "textfile": 2,
"value": 1, "v": 1,
"soundfiler": 0,
# GUI
"openpanel": 1, "savepanel": 1,
"cnv": 1,
"hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1,
"bng": 1, "tgl": 1, "nbx": 2, "vu": 0,
"print": 0,
# Time
"pipe": 1, "delay": 1, "del": 1,
"metro": 1, "tempo": 2,
"line": 1, "poly": 1,
# Send/receive
"s": 0, "send": 0, "r": 1, "receive": 1,
"netsend": 0, "netreceive": 1,
# Misc
"loadbang": 1, "loadb": 1, "bang": 1,
"inlet": 0, "outlet": 0,
"expr": 1, "clone": -1,
}