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,271 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user