3cc497f632
- midi-in.pd: netreceive -> route -> unpack -> pitch/velocity outlets - audio-out.pd: inlet~ -> dac~ abstraction - synth.pd: consumer patch wiring midi-in -> voice -> audio-out - led-matrix-viz.py: router socket non-blocking, registered with poller - pd.rego: skip outlet/abstraction false positives in validation
276 lines
8.4 KiB
Rego
276 lines
8.4 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 (skip unknown objects — might be abstractions with inlet~)
|
|
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)
|
|
known_inlets[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)
|
|
# Skip outlet objects (valid in abstractions) and unknown abstractions
|
|
violation contains msg if {
|
|
some rec in input.records
|
|
rec.type == "connect"
|
|
dst_rec := record_by_idx(rec.dst)
|
|
dst_rec.type == "obj"
|
|
dst_rec.name != "outlet"
|
|
dst_rec.name != "outlet~"
|
|
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,
|
|
}
|