Roundtrip parsing and generation of pure-data patches from python
Project description
py2pd - Python <-> PureData
Roundtrip parsing and generation of pure-data patches from python.
py2pd is a fork and extensive rewrite of Dylan Burati's puredata-compiler using some of the ideas from py2max.
Features
- Builder API -- imperative patch construction with
add(),link(), and automatic layout - AST API -- lossless round-trip parsing of
.pdfiles via frozen dataclasses - Bridging -- convert freely between Builder and AST with
from_builder()/to_builder() - All GUI types -- Bang, Toggle, NumberBox, Symbol, HSlider, VSlider, HRadio, VRadio, Canvas, VU
- Subpatches -- nested patches with auto-inferred inlet/outlet counts and graph-on-parent support
- Abstractions -- reference external
.pdfiles with auto-inferred I/O - Connection validation -- eager index checking against a registry of ~80 common Pd objects
- Patch optimization -- deduplicate connections, collapse pass-throughs, remove unused nodes
- SVG export -- visualize patches as SVG
- Externals discovery -- platform-aware scanning for
.pdabstractions and binary externals - libpd validation -- load patches into libpd via cypd and check for errors (
pip install py2pd[extras]) - hvcc integration -- validate and compile patches with the Heavy Compiler Collection (
pip install py2pd[extras]) - Zero runtime dependencies -- Python 3.13+, no required dependencies
Install
pip install py2pd
Quick Start
from py2pd import Patcher
# Create a simple synthesizer patch
p = Patcher('synth.pd')
osc = p.add('osc~ 440')
gain = p.add('*~ 0.3')
dac = p.add('dac~')
p.link(osc, gain)
p.link(gain, dac)
p.link(gain, dac, inlet=1) # stereo
p.save()
Builder API
The Patcher class provides methods to add nodes and connect them.
Adding Nodes
from py2pd import Patcher
p = Patcher('example.pd')
# Objects
osc = p.add('osc~ 440')
filter_obj = p.add('lop~ 1000')
# Messages
bang = p.add_msg('bang')
freq_msg = p.add_msg('440')
# GUI Elements
slider = p.add_hslider(min_val=20, max_val=20000, width=150)
toggle = p.add_toggle(default_value=1, send='onoff')
numbox = p.add_numberbox(min_val=0, max_val=127)
bang_btn = p.add_bang(send='trigger', label='Click')
Connecting Nodes
Use link() to connect nodes. By default, outlet 0 connects to inlet 0:
p.link(osc, gain) # outlet 0 -> inlet 0
p.link(gain, dac) # left channel
p.link(gain, dac, inlet=1) # right channel (stereo)
p.link(trigger, pack, outlet=1, inlet=2) # specific ports
Outlet and inlet indices are validated eagerly when the node's I/O counts are known (from PD_OBJECT_REGISTRY or explicit num_inlets/num_outlets). Out-of-range indices raise PdConnectionError at link() time rather than silently creating invalid connections. Objects with unknown I/O counts (e.g., trigger, pack, route) skip validation.
Subpatches
Create reusable subpatches:
def make_envelope() -> Patcher:
p = Patcher()
inlet = p.add('inlet')
vline = p.add('vline~')
outlet = p.add('outlet~')
p.link(inlet, vline)
p.link(vline, outlet)
return p
main = Patcher('main.pd')
osc = main.add('osc~ 440')
env = main.add_subpatch('envelope', make_envelope())
vca = main.add('*~')
main.link(osc, vca)
main.link(env, vca, inlet=1)
Graph-on-Parent
Subpatches can expose their GUI elements to the parent patch using graph-on-parent mode:
inner = Patcher()
slider = inner.add_hslider(min_val=0, max_val=1000, label='freq')
outlet = inner.add('outlet')
inner.link(slider, outlet)
main = Patcher('main.pd')
ctrl = main.add_subpatch(
'controls', inner,
graph_on_parent=True,
hide_name=True,
gop_width=150,
gop_height=40,
)
Abstractions
Reference external .pd files as objects in your patch:
from py2pd import Patcher
p = Patcher('main.pd')
# With explicit inlet/outlet counts
synth = p.add_abstraction('my-synth 440 0.5',
num_inlets=2, num_outlets=1)
# Auto-infer I/O from the source file
synth = p.add_abstraction('my-synth 440',
source_path='my-synth.pd')
dac = p.add('dac~')
p.link(synth, dac)
p.link(synth, dac, inlet=1)
Layout Options
Default layout - nodes flow top-to-bottom:
p = Patcher('patch.pd')
p.add('osc~ 440') # Row 1
p.add('*~ 0.5') # Row 2
p.add('dac~') # Row 3
Grid layout - organized columns:
from py2pd import Patcher, GridLayoutManager
grid = GridLayoutManager(columns=4, cell_width=80, cell_height=35)
p = Patcher('grid.pd', layout=grid)
Auto layout - arrange by signal flow:
p = Patcher('patch.pd')
# Add nodes in any order...
p.auto_layout(margin=50, row_spacing=50, col_spacing=100)
Saving and Export
p.save() # Save to filename from constructor
p.save('other.pd') # Save to specific file
p.save_svg('patch.svg') # Export visualization as SVG
svg_str = p.to_svg() # Get SVG as string
Optimization
Clean up patches by removing unused elements and simplifying connections:
p = Patcher()
osc = p.add('osc~ 440')
gain = p.add('*~ 0.5')
unused = p.add('+~ 0.1') # not connected to anything
dac = p.add('dac~')
p.link(osc, gain)
p.link(gain, dac)
p.link(gain, dac) # accidental duplicate
result = p.optimize()
# result == {'nodes_removed': 1, 'connections_removed': 2,
# 'duplicates_removed': 1, 'pass_throughs_collapsed': 0,
# 'subpatches_optimized': 0}
The three passes run in order:
- Deduplicate connections -- removes exact-duplicate patch cords.
- Pass-through collapse -- bypasses single-in/single-out nodes (opt-in via
collapsible_objects). - Unused element removal -- removes disconnected
Objnodes. GUI elements, comments, subpatches, abstractions, arrays, messages, and floats are never removed. Nodes with active send/receive parameters are preserved.
Use recursive=True to optimize inner subpatches as well:
result = p.optimize(recursive=True)
Validation
p.validate_connections(check_cycles=True) # Raises on invalid connections
AST API (Round-trip Parsing)
For modifying existing patches with immutable AST nodes:
from py2pd import parse_file, serialize
# Parse existing patch
ast = parse_file('input.pd')
# Modify the AST...
# Write back
with open('output.pd', 'w') as f:
f.write(serialize(ast))
Converting Between APIs
You can convert between AST and Builder representations:
from py2pd import parse_file, to_builder, from_builder
# AST -> Builder: parse then edit with the more convenient API
ast = parse_file('input.pd')
patch = to_builder(ast)
patch.add('osc~ 880')
patch.save('output.pd')
# Builder -> AST: for analysis or transformation
ast = from_builder(patch)
When to Use Each API
| Use Case | Recommended API |
|---|---|
| Creating patches from scratch | Builder |
| Modifying existing patches | Builder (via to_builder()) |
| Lossless round-trip of complex patches | AST |
| Building analysis/refactoring tools | AST |
| Batch search/replace across .pd files | AST |
For most workflows, parse to AST then convert to Builder for editing. Use the AST API directly when you need to preserve elements the Builder doesn't model (e.g., coords, comments) or need immutable transformations.
AST node types are available from the py2pd.ast module:
from py2pd.ast import PdPatch, PdObj, PdMsg, Position, transform, find_objects
GUI Elements
| Method | Description |
|---|---|
add_bang() |
Bang button |
add_toggle() |
On/off toggle |
add_numberbox() |
Editable number |
add_float() |
Float atom |
add_symbol() |
Symbol/text input |
add_hslider() |
Horizontal slider |
add_vslider() |
Vertical slider |
add_hradio() |
Horizontal radio buttons |
add_vradio() |
Vertical radio buttons |
add_canvas() |
Background/label area |
add_vu() |
VU meter |
All GUI add_* methods accept every parameter from the underlying constructor, including IEM styling options (label_x, label_y, font, font_size, bg_color, fg_color, label_color, etc.). Defaults match PureData's standard values.
Discovery
Scan the filesystem for installed PureData externals:
from py2pd import discover_externals, default_search_paths, extract_declare_paths
# Find all externals on platform-default search paths
registry = discover_externals()
# registry == {'my-external': (2, 1), 'reverb~': (None, None), ...}
# See which paths are searched
paths = default_search_paths()
# Extract -path declarations from a parsed patch
from py2pd import parse_file
ast = parse_file('input.pd')
declared = extract_declare_paths(ast)
discover_externals() returns a dict mapping external names to (num_inlets, num_outlets) tuples. For .pd abstractions the counts are inferred from the file; binary externals get (None, None).
Integrations
Validation (cypd/libpd)
Validate patches by loading them into libpd and checking for errors:
pip install py2pd[extras]
from py2pd.integrations.cypd import validate_patch
p = Patcher('synth.pd')
osc = p.add('osc~ 440')
dac = p.add('dac~')
p.link(osc, dac)
result = validate_patch(p)
if not result.ok:
print("Errors:", result.errors)
print("Warnings:", result.warnings)
hvcc (Heavy Compiler Collection)
Build patches compatible with the hvcc compiler for generating C/C++ code:
pip install py2pd[extras]
HeavyPatcher validates objects at add-time:
from py2pd.integrations.hvcc import HeavyPatcher, HvccGenerator
p = HeavyPatcher(generators=[HvccGenerator.DPF])
# add_param creates an [r __hv_param_name ...] receive object
freq = p.add_param('freq', min_val=20.0, max_val=20000.0, default=440.0)
osc = p.add('osc~')
dac = p.add('dac~')
p.link(freq, osc)
p.link(osc, dac)
p.link(osc, dac, inlet=1)
Validate existing patches:
from py2pd.integrations.hvcc import validate_for_hvcc
result = validate_for_hvcc(p)
if not result.ok:
print("Unsupported objects:", result.unsupported)
Compile to C/C++ (requires hvcc installed):
from py2pd.integrations.hvcc import compile_hvcc
result = compile_hvcc(p, name='MySynth', out_dir='build/')
if not result.ok:
print(result.stderr)
Error Handling
from py2pd import (
PdConnectionError, # Invalid connection arguments (including out-of-range indices)
NodeNotFoundError, # Node not in patch
InvalidConnectionError, # Bad inlet/outlet index (from validate_connections)
CycleWarning, # Feedback loop detected
)
PdConnectionError is raised eagerly by link() when outlet or inlet indices exceed the node's known I/O counts. Node.__getitem__ (e.g., osc[2]) raises ValueError for out-of-range outlet indices. Both checks are skipped for objects with unknown counts (num_outlets=None / num_inlets=None).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py2pd-0.1.3.tar.gz.
File metadata
- Download URL: py2pd-0.1.3.tar.gz
- Upload date:
- Size: 45.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1e9fc9703a4fdabf934517e70443b254046c39322f510a137d4577d88770ac9
|
|
| MD5 |
769363c1a591d4435a8215f243925c6b
|
|
| BLAKE2b-256 |
094e3859bf4f080bb3d39ce246a4c71c11427bc2787b3be4a08897174123422b
|
File details
Details for the file py2pd-0.1.3-py3-none-any.whl.
File metadata
- Download URL: py2pd-0.1.3-py3-none-any.whl
- Upload date:
- Size: 48.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83677267765f2238a973a7f0b51649c3b016b1b356b1492c67d80b409098fe0a
|
|
| MD5 |
403154b1c032889a1b2065d1d7355c9b
|
|
| BLAKE2b-256 |
a403ba7bad8fd5e97502ed701005c85fe1c970663ee02cdf36938bfbcc6db136
|