py2max 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py2max/__init__.py +67 -0
- py2max/__main__.py +6 -0
- py2max/cli.py +1251 -0
- py2max/core/__init__.py +39 -0
- py2max/core/abstract.py +146 -0
- py2max/core/box.py +231 -0
- py2max/core/common.py +19 -0
- py2max/core/patcher.py +1658 -0
- py2max/core/patchline.py +68 -0
- py2max/exceptions.py +385 -0
- py2max/export/__init__.py +20 -0
- py2max/export/converters.py +345 -0
- py2max/export/svg.py +393 -0
- py2max/layout/__init__.py +26 -0
- py2max/layout/base.py +463 -0
- py2max/layout/flow.py +405 -0
- py2max/layout/grid.py +374 -0
- py2max/layout/matrix.py +628 -0
- py2max/log.py +338 -0
- py2max/maxref/__init__.py +78 -0
- py2max/maxref/category.py +163 -0
- py2max/maxref/db.py +1082 -0
- py2max/maxref/legacy.py +324 -0
- py2max/maxref/parser.py +703 -0
- py2max/py.typed +0 -0
- py2max/server/__init__.py +54 -0
- py2max/server/client.py +295 -0
- py2max/server/inline.py +312 -0
- py2max/server/repl.py +561 -0
- py2max/server/rpc.py +240 -0
- py2max/server/websocket.py +997 -0
- py2max/static/cola.min.js +4 -0
- py2max/static/d3.v7.min.js +2 -0
- py2max/static/dagre-bundle.js +328 -0
- py2max/static/elk.bundled.js +6663 -0
- py2max/static/index.html +168 -0
- py2max/static/interactive.html +589 -0
- py2max/static/interactive.js +2111 -0
- py2max/static/live-preview.js +324 -0
- py2max/static/svg.min.js +13 -0
- py2max/static/svg.min.js.map +1 -0
- py2max/transformers.py +168 -0
- py2max/utils.py +83 -0
- py2max-0.2.1.dist-info/METADATA +390 -0
- py2max-0.2.1.dist-info/RECORD +48 -0
- py2max-0.2.1.dist-info/WHEEL +4 -0
- py2max-0.2.1.dist-info/entry_points.txt +3 -0
- py2max-0.2.1.dist-info/licenses/LICENSE +19 -0
py2max/transformers.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Composable patcher transformers for py2max."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Callable, Dict, Iterable, NamedTuple, Optional, Sequence
|
|
6
|
+
|
|
7
|
+
from .core import Patcher
|
|
8
|
+
|
|
9
|
+
Transformer = Callable[[Patcher], Patcher]
|
|
10
|
+
TransformerFactory = Callable[[Optional[str]], Transformer]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TransformerSpec(NamedTuple):
|
|
14
|
+
factory: TransformerFactory
|
|
15
|
+
description: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compose(transformers: Sequence[Transformer]) -> Transformer:
|
|
19
|
+
"""Compose multiple transformers into a single operation."""
|
|
20
|
+
|
|
21
|
+
def _composed(patcher: Patcher) -> Patcher:
|
|
22
|
+
current = patcher
|
|
23
|
+
for transform in transformers:
|
|
24
|
+
current = transform(current)
|
|
25
|
+
return current
|
|
26
|
+
|
|
27
|
+
return _composed
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_pipeline(patcher: Patcher, transformers: Iterable[Transformer]) -> Patcher:
|
|
31
|
+
"""Apply each transformer in order to the patcher."""
|
|
32
|
+
|
|
33
|
+
current = patcher
|
|
34
|
+
for transform in transformers:
|
|
35
|
+
current = transform(current)
|
|
36
|
+
return current
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def optimize_layout(layout: str | None = None) -> Transformer:
|
|
40
|
+
"""Return a transformer that optimizes layout with an optional override."""
|
|
41
|
+
|
|
42
|
+
def _transform(patcher: Patcher) -> Patcher:
|
|
43
|
+
if layout:
|
|
44
|
+
patcher._layout_mgr = patcher.set_layout_mgr(layout)
|
|
45
|
+
patcher.optimize_layout()
|
|
46
|
+
return patcher
|
|
47
|
+
|
|
48
|
+
return _transform
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_flow_direction(direction: str) -> Transformer:
|
|
52
|
+
"""Set the flow direction on the current layout manager."""
|
|
53
|
+
|
|
54
|
+
def _transform(patcher: Patcher) -> Patcher:
|
|
55
|
+
patcher._flow_direction = direction
|
|
56
|
+
if hasattr(patcher._layout_mgr, "flow_direction"):
|
|
57
|
+
patcher._layout_mgr.flow_direction = direction
|
|
58
|
+
return patcher
|
|
59
|
+
|
|
60
|
+
return _transform
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def add_comment_transform(comment: str, pos: str = "above") -> Transformer:
|
|
64
|
+
"""Attach a comment near every object in the patcher."""
|
|
65
|
+
|
|
66
|
+
def _transform(patcher: Patcher) -> Patcher:
|
|
67
|
+
for box in list(patcher._boxes):
|
|
68
|
+
if box.id:
|
|
69
|
+
patcher.add_associated_comment(box, comment, comment_pos=pos) # type: ignore[arg-type]
|
|
70
|
+
patcher._process_pending_comments()
|
|
71
|
+
return patcher
|
|
72
|
+
|
|
73
|
+
return _transform
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_font_size(size: float) -> Transformer:
|
|
77
|
+
"""Set default font size for the patcher and all boxes."""
|
|
78
|
+
|
|
79
|
+
def _transform(patcher: Patcher) -> Patcher:
|
|
80
|
+
patcher.default_fontsize = size
|
|
81
|
+
for box in patcher._boxes:
|
|
82
|
+
box._kwds["fontsize"] = size
|
|
83
|
+
return patcher
|
|
84
|
+
|
|
85
|
+
return _transform
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _set_font_size_factory(arg: Optional[str]) -> Transformer:
|
|
89
|
+
value = float(arg) if arg is not None else 12.0
|
|
90
|
+
return set_font_size(value)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _add_comment_factory(arg: Optional[str]) -> Transformer:
|
|
94
|
+
text = arg or "Auto"
|
|
95
|
+
return add_comment_transform(text)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _optimize_layout_factory(arg: Optional[str]) -> Transformer:
|
|
99
|
+
return optimize_layout(layout=arg)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _set_flow_direction_factory(arg: Optional[str]) -> Transformer:
|
|
103
|
+
direction = arg or "horizontal"
|
|
104
|
+
return set_flow_direction(direction)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_TRANSFORMERS: Dict[str, TransformerSpec] = {
|
|
108
|
+
"set-font-size": TransformerSpec(
|
|
109
|
+
_set_font_size_factory, "Set default font size (points). Default 12.0"
|
|
110
|
+
),
|
|
111
|
+
"add-comment": TransformerSpec(
|
|
112
|
+
_add_comment_factory, "Attach the supplied comment text above each box"
|
|
113
|
+
),
|
|
114
|
+
"optimize-layout": TransformerSpec(
|
|
115
|
+
_optimize_layout_factory,
|
|
116
|
+
"Run layout optimisation (optionally specify layout name)",
|
|
117
|
+
),
|
|
118
|
+
"set-flow-direction": TransformerSpec(
|
|
119
|
+
_set_flow_direction_factory,
|
|
120
|
+
"Adjust flow direction for the active layout manager (horizontal/vertical/column)",
|
|
121
|
+
),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def available_transformers() -> Dict[str, str]:
|
|
126
|
+
"""Return available transformer names mapped to short descriptions."""
|
|
127
|
+
|
|
128
|
+
return {name: spec.description for name, spec in _TRANSFORMERS.items()}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_transformer(name: str, argument: Optional[str] = None) -> Transformer:
|
|
132
|
+
"""Instantiate a transformer by name.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
name: Registered transformer name.
|
|
136
|
+
argument: Optional string argument passed to the transformer factory.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
KeyError: If the transformer name is unknown.
|
|
140
|
+
ValueError: If the argument cannot be parsed.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
spec = _TRANSFORMERS[name]
|
|
145
|
+
except KeyError as exc: # pragma: no cover - defensive
|
|
146
|
+
raise KeyError(f"Unknown transformer '{name}'") from exc
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
return spec.factory(argument)
|
|
150
|
+
except ValueError as exc: # pragma: no cover - argument parsing failure
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Invalid argument for transformer '{name}': {argument}"
|
|
153
|
+
) from exc
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"Transformer",
|
|
158
|
+
"TransformerFactory",
|
|
159
|
+
"TransformerSpec",
|
|
160
|
+
"compose",
|
|
161
|
+
"run_pipeline",
|
|
162
|
+
"optimize_layout",
|
|
163
|
+
"set_flow_direction",
|
|
164
|
+
"add_comment_transform",
|
|
165
|
+
"set_font_size",
|
|
166
|
+
"available_transformers",
|
|
167
|
+
"create_transformer",
|
|
168
|
+
]
|
py2max/utils.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Utility functions for py2max.
|
|
2
|
+
|
|
3
|
+
This module provides helper functions for common Max/MSP operations
|
|
4
|
+
such as pitch-to-frequency conversion and other musical calculations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
NOTE_TO_SEMITONE = {
|
|
8
|
+
"C": 0,
|
|
9
|
+
"C#": 1,
|
|
10
|
+
"Db": 1,
|
|
11
|
+
"D": 2,
|
|
12
|
+
"D#": 3,
|
|
13
|
+
"Eb": 3,
|
|
14
|
+
"E": 4,
|
|
15
|
+
"Fb": 4,
|
|
16
|
+
"E#": 5,
|
|
17
|
+
"F": 5,
|
|
18
|
+
"F#": 6,
|
|
19
|
+
"Gb": 6,
|
|
20
|
+
"G": 7,
|
|
21
|
+
"G#": 8,
|
|
22
|
+
"Ab": 8,
|
|
23
|
+
"A": 9,
|
|
24
|
+
"A#": 10,
|
|
25
|
+
"Bb": 10,
|
|
26
|
+
"B": 11,
|
|
27
|
+
"Cb": 11,
|
|
28
|
+
"B#": 0,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def pitch2freq(pitch, A4=440):
|
|
33
|
+
"""Convert a pitch name to a frequency in Hz.
|
|
34
|
+
|
|
35
|
+
Converts standard pitch notation (e.g., "C4", "A#3") to frequency
|
|
36
|
+
values using equal temperament tuning.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
pitch: Pitch name in format like "C4", "A#3", "Bb2".
|
|
40
|
+
A4: Reference frequency for A4 (default: 440 Hz).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Frequency in Hz as a float.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> pitch2freq("C3")
|
|
47
|
+
130.8127826502993
|
|
48
|
+
>>> pitch2freq("A4")
|
|
49
|
+
440.0
|
|
50
|
+
|
|
51
|
+
Note:
|
|
52
|
+
Based on: https://gist.github.com/CGrassin/26a1fdf4fc5de788da9b376ff717516e
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
token = pitch.strip()
|
|
56
|
+
if len(token) < 2:
|
|
57
|
+
raise ValueError(f"Invalid pitch name: '{pitch}'")
|
|
58
|
+
|
|
59
|
+
note = token[0].upper()
|
|
60
|
+
idx = 1
|
|
61
|
+
|
|
62
|
+
if idx < len(token) and token[idx] in ("#", "b", "♯", "♭"):
|
|
63
|
+
accidental = token[idx]
|
|
64
|
+
if accidental == "♯":
|
|
65
|
+
accidental = "#"
|
|
66
|
+
elif accidental == "♭":
|
|
67
|
+
accidental = "b"
|
|
68
|
+
note += accidental
|
|
69
|
+
idx += 1
|
|
70
|
+
|
|
71
|
+
octave_part = token[idx:]
|
|
72
|
+
try:
|
|
73
|
+
octave = int(octave_part)
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
raise ValueError(f"Invalid octave in pitch name: '{pitch}'") from exc
|
|
76
|
+
|
|
77
|
+
if note not in NOTE_TO_SEMITONE:
|
|
78
|
+
raise ValueError(f"Unsupported note name: '{pitch}'")
|
|
79
|
+
|
|
80
|
+
semitone = NOTE_TO_SEMITONE[note]
|
|
81
|
+
midi_number = semitone + (octave + 1) * 12
|
|
82
|
+
distance_from_A4 = midi_number - 69
|
|
83
|
+
return float(A4) * 2 ** (distance_from_A4 / 12)
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py2max
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: A library for offline generation of Max/MSP patcher (.maxpat) files.
|
|
5
|
+
Keywords: Max,maxpat,offline
|
|
6
|
+
Author: Shakeeb Alireza
|
|
7
|
+
Author-email: Shakeeb Alireza <shakfu@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Operating System :: OS Independent
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Dist: websockets>=12.0 ; extra == 'server'
|
|
26
|
+
Requires-Dist: ptpython>=3.0.0 ; extra == 'server'
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Project-URL: Changelog, https://github.com/shakfu/py2max/blob/main/CHANGELOG.md
|
|
29
|
+
Project-URL: Documentation, https://github.com/shakfu/py2max#readme
|
|
30
|
+
Project-URL: Homepage, https://github.com/shakfu/py2max
|
|
31
|
+
Project-URL: Issues, https://github.com/shakfu/py2max/issues
|
|
32
|
+
Project-URL: Repository, https://github.com/shakfu/py2max.git
|
|
33
|
+
Provides-Extra: server
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# py2max
|
|
37
|
+
|
|
38
|
+
[](https://www.python.org/downloads/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
41
|
+
A pure Python library for offline generation of Max/MSP patcher files (`.maxpat`, `.maxhelp`, `.rbnopat`).
|
|
42
|
+
|
|
43
|
+
If you are looking for Python 3 externals for Max/MSP, check out the [py-js](https://github.com/shakfu/py-js) project.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install py2max
|
|
49
|
+
|
|
50
|
+
# With interactive server support
|
|
51
|
+
pip install py2max[server]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For development:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/shakfu/py2max.git
|
|
58
|
+
cd py2max
|
|
59
|
+
uv sync
|
|
60
|
+
source .venv/bin/activate
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from py2max import Patcher
|
|
67
|
+
|
|
68
|
+
p = Patcher('my-synth.maxpat')
|
|
69
|
+
osc = p.add('cycle~ 440')
|
|
70
|
+
gain = p.add('gain~')
|
|
71
|
+
dac = p.add('ezdac~')
|
|
72
|
+
p.link(osc, gain)
|
|
73
|
+
p.link(gain, dac)
|
|
74
|
+
p.save()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That's it! Open `my-synth.maxpat` in Max to see your patch.
|
|
78
|
+
|
|
79
|
+
## Features
|
|
80
|
+
|
|
81
|
+
### Core Capabilities
|
|
82
|
+
|
|
83
|
+
- **Offline Patch Generation** - Create Max patches programmatically without Max running
|
|
84
|
+
- **Round-trip Conversion** - Load, modify, and save existing `.maxpat` files
|
|
85
|
+
- **Universal Object Support** - Works with any Max/MSP/Jitter object
|
|
86
|
+
- **99% Test Coverage** - 418+ tests ensure reliability
|
|
87
|
+
|
|
88
|
+
### Interactive Server (New in 0.2.x)
|
|
89
|
+
|
|
90
|
+
Real-time browser-based patch editing with bidirectional sync:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
py2max serve my-patch.maxpat
|
|
94
|
+
# Opens browser at http://localhost:8000
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Features:**
|
|
98
|
+
|
|
99
|
+
- Drag objects, draw connections visually
|
|
100
|
+
- Three layout engines: **WebCola**, **ELK**, and **Dagre**
|
|
101
|
+
- Auto-save with debouncing
|
|
102
|
+
- Navigate into subpatchers
|
|
103
|
+
- REPL mode for Python interaction
|
|
104
|
+
|
|
105
|
+
### SVG Preview
|
|
106
|
+
|
|
107
|
+
Generate high-quality SVG previews without Max:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
py2max preview my-patch.maxpat --open
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
p = Patcher('synth.maxpat')
|
|
115
|
+
# ... add objects ...
|
|
116
|
+
p.to_svg('synth.svg', title="My Synth", show_ports=True)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Layout Managers
|
|
120
|
+
|
|
121
|
+
Five built-in layout strategies:
|
|
122
|
+
|
|
123
|
+
| Layout | Description |
|
|
124
|
+
|--------|-------------|
|
|
125
|
+
| `grid` | Connection-aware clustering with configurable flow |
|
|
126
|
+
| `flow` | Signal flow-based hierarchical positioning |
|
|
127
|
+
| `columnar` | Controls -> Generators -> Processors -> Outputs |
|
|
128
|
+
| `matrix` | Signal chains in columns, categories in rows |
|
|
129
|
+
| `horizontal`/`vertical` | Simple grid layouts |
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
p = Patcher('patch.maxpat', layout='flow', flow_direction='vertical')
|
|
133
|
+
# Add objects and connections...
|
|
134
|
+
p.optimize_layout() # Arrange based on signal flow
|
|
135
|
+
p.save()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### MaxRef Integration
|
|
139
|
+
|
|
140
|
+
Access documentation for 1157 Max objects:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
p = Patcher('demo.maxpat')
|
|
144
|
+
cycle = p.add('cycle~ 440')
|
|
145
|
+
|
|
146
|
+
print(cycle.help()) # Full documentation
|
|
147
|
+
print(f"Inlets: {cycle.get_inlet_count()}")
|
|
148
|
+
print(f"Outlets: {cycle.get_outlet_count()}")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Connection Validation
|
|
152
|
+
|
|
153
|
+
Optional validation catches wiring errors:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
p = Patcher('patch.maxpat', validate_connections=True)
|
|
157
|
+
osc = p.add('cycle~ 440')
|
|
158
|
+
gain = p.add('gain~')
|
|
159
|
+
|
|
160
|
+
p.link(osc, gain) # Valid
|
|
161
|
+
p.link(osc, gain, outlet=5) # Raises InvalidConnectionError
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Semantic IDs
|
|
165
|
+
|
|
166
|
+
Human-readable object IDs for easier debugging:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
p = Patcher('patch.maxpat', semantic_ids=True)
|
|
170
|
+
|
|
171
|
+
osc1 = p.add('cycle~ 440') # ID: 'cycle_1'
|
|
172
|
+
osc2 = p.add('cycle~ 220') # ID: 'cycle_2'
|
|
173
|
+
gain = p.add('gain~') # ID: 'gain_1'
|
|
174
|
+
|
|
175
|
+
# Find by semantic ID
|
|
176
|
+
osc = p.find_by_id('cycle_1')
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### SQLite Database
|
|
180
|
+
|
|
181
|
+
Query Max object metadata efficiently:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from py2max import MaxRefDB
|
|
185
|
+
|
|
186
|
+
db = MaxRefDB() # Auto-cached on first use
|
|
187
|
+
print(len(db)) # 1157 objects
|
|
188
|
+
|
|
189
|
+
if 'cycle~' in db:
|
|
190
|
+
info = db['cycle~']
|
|
191
|
+
print(info['digest'])
|
|
192
|
+
|
|
193
|
+
# Search and filter
|
|
194
|
+
results = db.search('filter')
|
|
195
|
+
msp_objects = db.by_category('MSP')
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Usage Examples
|
|
199
|
+
|
|
200
|
+
### Basic Patch Creation
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from py2max import Patcher
|
|
204
|
+
|
|
205
|
+
p = Patcher('my-patch.maxpat')
|
|
206
|
+
osc = p.add('cycle~ 440')
|
|
207
|
+
gain = p.add('gain~')
|
|
208
|
+
dac = p.add('ezdac~')
|
|
209
|
+
|
|
210
|
+
p.link(osc, gain)
|
|
211
|
+
p.link(gain, dac)
|
|
212
|
+
p.link(gain, dac, inlet=1) # Stereo
|
|
213
|
+
p.save()
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Loading and Modifying Patches
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
p = Patcher.from_file('existing.maxpat')
|
|
220
|
+
|
|
221
|
+
# Find and modify objects
|
|
222
|
+
for box in p.find_by_text('cycle~'):
|
|
223
|
+
print(f"Found oscillator: {box.id}")
|
|
224
|
+
|
|
225
|
+
p.save_as('modified.maxpat')
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Subpatchers
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
p = Patcher('main.maxpat')
|
|
232
|
+
sbox = p.add_subpatcher('p mysub')
|
|
233
|
+
sp = sbox.subpatcher
|
|
234
|
+
|
|
235
|
+
# Build the subpatcher
|
|
236
|
+
inlet = sp.add('inlet')
|
|
237
|
+
gain = sp.add('gain~')
|
|
238
|
+
outlet = sp.add('outlet')
|
|
239
|
+
sp.link(inlet, gain)
|
|
240
|
+
sp.link(gain, outlet)
|
|
241
|
+
|
|
242
|
+
# Connect in main patcher
|
|
243
|
+
osc = p.add('cycle~ 440')
|
|
244
|
+
dac = p.add('ezdac~')
|
|
245
|
+
p.link(osc, sbox)
|
|
246
|
+
p.link(sbox, dac)
|
|
247
|
+
p.save()
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Object Search
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
p = Patcher.from_file('complex-patch.maxpat')
|
|
254
|
+
|
|
255
|
+
# Find by ID
|
|
256
|
+
obj = p.find_by_id('obj-5')
|
|
257
|
+
|
|
258
|
+
# Find by text content
|
|
259
|
+
oscillators = p.find_by_text('cycle~')
|
|
260
|
+
|
|
261
|
+
# Find by object type
|
|
262
|
+
messages = p.find_by_type('message')
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Command Line Interface
|
|
266
|
+
|
|
267
|
+
### Patch Management
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# Create new patch from template
|
|
271
|
+
py2max new demo.maxpat --template stereo
|
|
272
|
+
|
|
273
|
+
# Show patch info
|
|
274
|
+
py2max info demo.maxpat
|
|
275
|
+
|
|
276
|
+
# Generate SVG preview
|
|
277
|
+
py2max preview demo.maxpat --open
|
|
278
|
+
|
|
279
|
+
# Optimize layout
|
|
280
|
+
py2max optimize demo.maxpat --layout flow
|
|
281
|
+
|
|
282
|
+
# Validate connections
|
|
283
|
+
py2max validate demo.maxpat
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Interactive Server
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# Start server with browser editing
|
|
290
|
+
py2max serve my-patch.maxpat
|
|
291
|
+
|
|
292
|
+
# With REPL in same terminal
|
|
293
|
+
py2max serve my-patch.maxpat --repl
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### MaxRef Database
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Show cache status
|
|
300
|
+
py2max db cache location
|
|
301
|
+
|
|
302
|
+
# Create category-specific database
|
|
303
|
+
py2max db create msp.db --category msp
|
|
304
|
+
|
|
305
|
+
# Search objects
|
|
306
|
+
py2max db search maxref.db "oscillator" -v
|
|
307
|
+
|
|
308
|
+
# Query specific object
|
|
309
|
+
py2max db query maxref.db cycle~ --json
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Converters
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Convert .maxpat to Python code
|
|
316
|
+
py2max convert maxpat-to-python patch.maxpat output.py
|
|
317
|
+
|
|
318
|
+
# Lookup object documentation
|
|
319
|
+
py2max maxref cycle~ --json
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Use Cases
|
|
323
|
+
|
|
324
|
+
- **Scripted patch generation** - Automate repetitive patch creation
|
|
325
|
+
- **Batch processing** - Modify multiple `.maxpat` files programmatically
|
|
326
|
+
- **Parametric patches** - Generate variations from configuration files
|
|
327
|
+
- **Test generation** - Create `.maxhelp` files during external development
|
|
328
|
+
- **Container population** - Prepopulate `coll`, `dict`, `table` objects with data
|
|
329
|
+
- **Generative patching** - Algorithmic patch creation
|
|
330
|
+
- **CI/CD integration** - SVG previews for documentation and version control
|
|
331
|
+
|
|
332
|
+
## Testing
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
make test # Run all tests
|
|
336
|
+
make typecheck # Type checking with mypy
|
|
337
|
+
make lint # Linting with ruff
|
|
338
|
+
make docs # Build documentation
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Design Notes
|
|
342
|
+
|
|
343
|
+
The `.maxpat` JSON format maps directly to three Python classes:
|
|
344
|
+
|
|
345
|
+
- **`Patcher`** - The patch container with boxes and patchlines
|
|
346
|
+
- **`Box`** - Individual Max objects
|
|
347
|
+
- **`Patchline`** - Connections between boxes
|
|
348
|
+
|
|
349
|
+
All classes are extendable via `**kwargs`, allowing any Max object configuration. The `add_textbox()` method handles most objects, with specialized methods (`add_subpatcher()`, `add_coll()`, etc.) for objects requiring extra configuration.
|
|
350
|
+
|
|
351
|
+
## Caveats
|
|
352
|
+
|
|
353
|
+
- Max doesn't refresh from file when open - close and reopen to see changes, or use `py2max serve` for live editing
|
|
354
|
+
- For tilde variants, use the `_tilde` suffix: `p.add_gen()` vs `p.add_gen_tilde()`
|
|
355
|
+
- API docs in progress - see `CLAUDE.md` for comprehensive usage
|
|
356
|
+
|
|
357
|
+
## Examples
|
|
358
|
+
|
|
359
|
+
See the `examples/` directory for demonstrations:
|
|
360
|
+
|
|
361
|
+
- `auto_layout_demo.py` - Complex synthesizer with layout optimization
|
|
362
|
+
- `nested_patcher_demo.py` - Subpatcher navigation
|
|
363
|
+
- `columnar_layout_demo.py` - Functional column organization
|
|
364
|
+
- `matrix_layout_demo.py` - Signal chain matrix layout
|
|
365
|
+
|
|
366
|
+
External usage:
|
|
367
|
+
|
|
368
|
+
- [faust2rnbo](https://github.com/grame-cncm/faust/blob/master-dev/architecture/max-msp/rnbo.py) - Generate Max patchers for RNBO
|
|
369
|
+
|
|
370
|
+
## Contributing
|
|
371
|
+
|
|
372
|
+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
git clone https://github.com/shakfu/py2max.git
|
|
376
|
+
cd py2max
|
|
377
|
+
uv sync
|
|
378
|
+
source .venv/bin/activate
|
|
379
|
+
make test # Verify setup
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## License
|
|
383
|
+
|
|
384
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
385
|
+
|
|
386
|
+
## Credits
|
|
387
|
+
|
|
388
|
+
- HOLA algorithm: Kieffer, Dwyer, Marriott, Wybrow (IEEE 2016)
|
|
389
|
+
- NetworkX: Hagberg, Schult, Swart (SciPy 2008)
|
|
390
|
+
- Graph drawing techniques: Gansner, Koutsofios, North, Vo (IEEE 1993)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
py2max/__init__.py,sha256=CvkhiHI_fmkjcTNXSYbkdPVxBV3QxL9TUEvlrZo0fJo,1994
|
|
2
|
+
py2max/__main__.py,sha256=dO6lxrMrGVDBd8DWi9tuD5s1XPMrcEPL50VSX-UT3u4,119
|
|
3
|
+
py2max/cli.py,sha256=2yDj3r_l98ZQn8B19ifIy5rVkhEYRYs6Wnh0_BjPA5E,40217
|
|
4
|
+
py2max/core/__init__.py,sha256=fsQW2xtkdQMpQjd9Ki_gFlKEtOZKL6diKil8hECaD4Y,995
|
|
5
|
+
py2max/core/abstract.py,sha256=y9p-Q0Sridsq2_K0fX4-x5gQvLHKLolsfGCkMwnfhCs,3848
|
|
6
|
+
py2max/core/box.py,sha256=ap4gfbIW6NlWwyy-IBxa7lLQi3Pq2tgGWmUWSf4fYCg,7336
|
|
7
|
+
py2max/core/common.py,sha256=cKNf0YiPMbmchBdJMNZ9oTS9duBIP8R-a3go5E__JIE,474
|
|
8
|
+
py2max/core/patcher.py,sha256=Ff_6GgSlQpHTmNXYzT510lvR-wKPz9SgKwh5t2JhW-k,57922
|
|
9
|
+
py2max/core/patchline.py,sha256=CABM1AljmMHqJXd0Iitq94MPg17f_eixRko_T1XMA4U,2056
|
|
10
|
+
py2max/exceptions.py,sha256=t0D_H-y9uqYYHPgPIA10V0elrkfELkvIvTkF5GbYnp0,10886
|
|
11
|
+
py2max/export/__init__.py,sha256=m7aiFGN-iVLLcn60Bx2WUzqUC1V0joxa3eNwlBEHpFA,502
|
|
12
|
+
py2max/export/converters.py,sha256=0MvqcJJrCMsGvzdBo_xQBNuXir_WnSxcuc0fVzs9IuA,10333
|
|
13
|
+
py2max/export/svg.py,sha256=XKF4ZSXZVVoshSIMbii3Ue96cjl_z8CP28kTYHao0DM,11592
|
|
14
|
+
py2max/layout/__init__.py,sha256=cG2dOm5NCW7FA65I7F0f_r2XKvpiF6yc4gIylxd13h0,893
|
|
15
|
+
py2max/layout/base.py,sha256=87SCXFHeJiw5UIRX5cHGDAZnrXo9D-tCliNXHV4uiyA,16571
|
|
16
|
+
py2max/layout/flow.py,sha256=xYhybUmk7gB74tl6DvU3yi3unitPzWLhEiUuHkV_IJA,15764
|
|
17
|
+
py2max/layout/grid.py,sha256=k4JnjJ9wqLdSBFT4YTNaHUX1DMrD-BeH7K7Fwa3U-PE,15054
|
|
18
|
+
py2max/layout/matrix.py,sha256=S8HnOjLpGDFsW86PZhYCxE2DjRTQPIiQzzVsmid3U_k,23966
|
|
19
|
+
py2max/log.py,sha256=pOChtQaydUyhPKvcxRxmoTCz24bSI7v3PhOWvX22Ofo,10118
|
|
20
|
+
py2max/maxref/__init__.py,sha256=FJy0iSBttIHtP_QvaT1zOtGuxJf6S274nU8P57UH5KI,1916
|
|
21
|
+
py2max/maxref/category.py,sha256=gxJAdk1Ruv66iEqpZRF7nN358ZuMJBQG5sHKtICfVz4,2698
|
|
22
|
+
py2max/maxref/db.py,sha256=KvGPNUzTJG_B_NtQ2vw3nLYo3gNaPVsxWYh1Ee0_Lk0,39365
|
|
23
|
+
py2max/maxref/legacy.py,sha256=uaG1kwkqIl8EuMNhfrs4fQxh8BI2g6IJGbOKMlaX1wM,9571
|
|
24
|
+
py2max/maxref/parser.py,sha256=Su95KXCPiSzrSioWdcEEx47Bi05I2TeuQtmEvWjNxLM,26913
|
|
25
|
+
py2max/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
py2max/server/__init__.py,sha256=RMx5_Yqp_PXPKm2EkEh4mH4eRMBnUWqj61oD8sJ4khk,1415
|
|
27
|
+
py2max/server/client.py,sha256=Io1ywNjXl63NJfMyTTh9S9Q_0nPq_dmjebuaPv7MdKw,9601
|
|
28
|
+
py2max/server/inline.py,sha256=bRFmg9Dm-dqfXRhgdQo3D4lIB2TLVi8FzwF9i702kXw,9894
|
|
29
|
+
py2max/server/repl.py,sha256=oLyAlZiMkg0VKiSMCBAlzxl53Rn1S5c9pJqIUws7Kuw,17483
|
|
30
|
+
py2max/server/rpc.py,sha256=nuYblQ2KXN7prlWLEbPchsBgrIFUoKNOyZujPgeTZaw,7362
|
|
31
|
+
py2max/server/websocket.py,sha256=E7GgOP78klpJEjz7dIcGEWCnPPSujgxY_U5gQv9ftvU,34584
|
|
32
|
+
py2max/static/cola.min.js,sha256=mDG9_3AMjpWdMq0_FA4VpiZohZiseIMlzfhEFLobSS0,79920
|
|
33
|
+
py2max/static/d3.v7.min.js,sha256=8glLv2FBs1lyLE_kVOtsSw8OQswQzHr5IfwVj864ZTk,279706
|
|
34
|
+
py2max/static/dagre-bundle.js,sha256=nthMZbMQ8QAocYVWEj7LBqX1jcxGFMguf5rItidDEss,60075
|
|
35
|
+
py2max/static/elk.bundled.js,sha256=y_YbAYLpCF023NWzkvV8yBYnMWmsQL3oC1K4CERMXPg,1627496
|
|
36
|
+
py2max/static/index.html,sha256=Xy30GBVJCEzKWttg1O8YPlpkN5kwXLeEakNQnfneEcI,4221
|
|
37
|
+
py2max/static/interactive.html,sha256=XCrSQ4JHe7k8kxwek7niTDwZ7sOPIdOwUelba9s0c0Y,20841
|
|
38
|
+
py2max/static/interactive.js,sha256=Uv5Gs_QJslYQwJRvMe9D0AD59RRSfpAjZnjwRe2bwL8,79071
|
|
39
|
+
py2max/static/live-preview.js,sha256=rnEDaphwP9qz1gS9p-Pq8zdWc3Yv5ulcn3mRJfKq174,10325
|
|
40
|
+
py2max/static/svg.min.js,sha256=hGPJhrshVc5fAwOf74KsM4FmRgVFNGgfJoBjroxUa_I,79567
|
|
41
|
+
py2max/static/svg.min.js.map,sha256=AZ9-hHUj_yRBXB9Ie6T0YqmPsbbdkGT2Cw5zFEpUcPs,330608
|
|
42
|
+
py2max/transformers.py,sha256=CRBeAKSSx0VyQnkTX9qHmd3XBprxrQhWykrPCQHwh6Q,4909
|
|
43
|
+
py2max/utils.py,sha256=zBxw1pGK89po3QK-TOcf6diuLqpVtC3FZchzyfY7WaQ,1941
|
|
44
|
+
py2max-0.2.1.dist-info/licenses/LICENSE,sha256=tBlNvY4s8JHbskvSQJnSBQWSPXdvEJv7rwUxDE7d0iA,1059
|
|
45
|
+
py2max-0.2.1.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
|
|
46
|
+
py2max-0.2.1.dist-info/entry_points.txt,sha256=uQFA1FvLH7eCa2S5bQYGdESNy6X8vbuc_cEal3ZlwbY,49
|
|
47
|
+
py2max-0.2.1.dist-info/METADATA,sha256=Hw96EV2vsipCzUBOWajlxZDNKgxZNa1J9yNxxV-E2Xw,9734
|
|
48
|
+
py2max-0.2.1.dist-info/RECORD,,
|