https://github.com/shakfu/py2max
A python library for offline generation of Max/MSP patcher (.maxpat) files.
https://github.com/shakfu/py2max
max maxmsp python python3
Last synced: 3 months ago
JSON representation
A python library for offline generation of Max/MSP patcher (.maxpat) files.
- Host: GitHub
- URL: https://github.com/shakfu/py2max
- Owner: shakfu
- License: mit
- Created: 2021-05-01T01:19:55.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2025-05-30T00:03:49.000Z (4 months ago)
- Last Synced: 2025-05-30T01:18:44.174Z (4 months ago)
- Topics: max, maxmsp, python, python3
- Language: Python
- Homepage: https://github.com/shakfu/py2max
- Size: 2.13 MB
- Stars: 32
- Watchers: 4
- Forks: 3
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# py2max
A pure python3 library without dependencies intended to facilitate the offline generation of Max patcher files (`.maxpat`, `.maxhelp`, `.rbnopat`).
If you are looking for python3 externals for Max/MSP check out the [py-js](https://github.com/shakfu/py-js) project.
## Features
- Scripted *offline* generation of Max patcher files using Python objects, corresponding, on a one-to-one basis, with Max/MSP objects stored in the `.maxpat` JSON-based file format.
- *Round-trip conversion* between (JSON) `.maxpat` files with arbitrary levels of nesting and corresponding `Patcher`, `Box`, and `Patchline` Python objects.
- Can potentially handle any Max object or maxclass.
- Lots of unit tests, `~99%` coverage.
- Analysis and offline scripted modification of Max patches in terms of composition, structure (as graphs of objects), object properties and layout (using graph-drawing algorithms).
- Allows precise layout and configuration of Max objects.
- Provide an object model which mirrors Max's patch organization: `Patcher` objects house `Box` objects and `PatchLine` objects that link them together, with support for nested `Patcher` objects as subpatches.
## Possible Use Cases
- Scripted patcher file creation.
- Batch modification of existing `.maxpat` files.
- Use the rich python standard library and ecosystem to help create parametrizable objects with configuration from offline sources. For example, one-of-a-kind wavetable oscillators configured from random wavetable files.
- Generation of test cases and `.maxhelp` files during external development
- Takes the pain out of creating objects with lots of parameters
- Prepopulate containers objects such as `coll`, `dict` and `table` objects with data
- Help to save time creating many objects with slightly different arguments
- Use [graph drawing / layout algorithms](docs/auto-layouts.md) on generated patches.
- Generative patch generation `(-;`
- etc..
## Usage examples
```python
p = Patcher('my-patch.maxpat')
osc1 = p.add_textbox('cycle~ 440')
gain = p.add_textbox('gain~')
dac = p.add_textbox('ezdac~')
osc1_gain = p.add_line(osc1, gain) # osc1 outlet 0 -> gain inlet 0
gain_dac0 = p.add_line(gain, dac, outlet=0, inlet=0)
gain_dac1 = p.add_line(gain, dac, outlet=0, inlet=1)
p.save()
```By default, objects are returned (including patchlines), and patchline outlets and inlets are set to 0. While returned objects are useful for linking, the returned patchlines are not. Therefore, the above can be written more concisely as:
```python
p = Patcher('my-patch.maxpat')
osc1 = p.add_textbox('cycle~ 440')
gain = p.add_textbox('gain~')
dac = p.add_textbox('ezdac~')
p.add_line(osc1, gain)
p.add_line(gain, dac)
p.add_line(gain, dac, inlet=1)
p.save()
```With builtin aliases (`.add` for `.add_*` type methods and `.link` for `.add_line`), the above example can be written in an even more abbreviated form (and with a vertical layout) as:
```python
p = Patcher('out_vertical.maxpat', layout='vertical')
osc = p.add('cycle~ 440')
gain = p.add('gain~')
dac = p.add('ezdac~')
p.link(osc, gain)
p.link(gain, dac)
p.link(gain, dac, 1)
p.save()
```In addition, you can parse existing `.maxpat` files, change them and then save the changes:
```python
p = Patcher.from_file('example1.maxpat')
# ... make some change
p.save_as('example1_mod.maxpat')
```Another example with subpatchers:
```python
p = Patcher('out.maxpat')
sbox = p.add_subpatcher('p mysub')
sp = sbox.subpatcher
in1 = sp.add('inlet')
gain = sp.add('gain~')
out1 = sp.add('outlet')
osc = p.add('cycle~ 440')
dac = p.add('ezdac~')
sp.link(in1, gain)
sp.link(gain, out1)
p.link(osc, sbox)
p.link(sbox, dac)
p.save()
```Note that Python classes are basically just simple wrappers around the JSON structures in a .maxpat file, and almost all Max/MSP and Jitter objects can be added to the patcher file with the `.add_textbox` or the generic `.add` methods. There are also specialized methods in the form `.add_` for numbers, numeric parameters, subpatchers, and container-type objects (see the design notes below for more details).
## Installation
Simplest way is to use [uv](https://github.com/astral-sh/uv):
```sh
git clone https://github.com/shakfu/py2max.git
cd py2max
uv sync
source .venv/bin/activate
```Note that py2max does not need to be installed to be used, so you can skip the `pip install` part if you prefer and just `cd` into the cloned directory and start using it interactively:
```sh
cd py2max
uv run python
```For example
```python
>>> from py2max import Patcher
>>> p = Patcher.from_file("tests/data/simple.maxpat")
>>> p.boxes
[Box(id='obj-2', text=None, maxclass='ezdac~', numinlets=2, numoutlets=0, outlettype=[''], patching_rect=Rect(x=284.0, y=272.0, w=45.0, h=45.0), patcher=None), Box(id='obj-1', text='cycle~ 440', maxclass='newobj', numinlets=2, numoutlets=1, outlettype=['signal'], patching_rect=Rect(x=279.0, y=149.0, w=66.0, h=22.0), patcher=None, varname='osc1')]
```## Quickstart
py2max has a minimal `Makefile` frontend to provide easy access to common commands:
```Makefile
.PHONY: all build test coverage clean resetall: build
build:
@uv buildtest:
@uv run pytestcoverage:
@mkdir -p outputs
@uv run pytest --cov-report html:outputs/_covhtml --cov=py2max testsclean:
@rm -rf outputs .*_cachereset: clean
@rm -rf .venv
```## Testing
`py2max` has an extensive test suite with tests in the `py2max/tests` folder.
One can run all tests as follows:
```sh
uv run pytest
```This will output the results of all tests into `outputs` folder.
Note that some tests may be skipped if a required package for the test cannot be imported.
You can check which test is skipped by the following:
```sh
uv run pytest -v
```To check test coverage:
```sh
make test
```which essentially does the following
```sh
mkdir -p outputs
uv run pytest --cov-report html:outputs/_covhtml --cov=py2max tests
```To run an individual test:
```sh
uv run pytest tests.test_basic
```Note that because `py2max` primarily deals with `json` generation and manipulation, most tests have no dependencies since `json` is already built into the stdlib.
However, a bunch of tests explore the application of orthogonal graph layout algorithms and for this, a whole bunch of packages have been used, which range from the well-known to the esoteric.
As mentioned above, pytest will skip a test if required packages are not installed, so these are entirely optional tests.
If you insist on diving into the rabbit hole, and want to run all tests you will need the following packages (and their dependencies):
- [networkx](https://networkx.org): `pip install networkx`
- [matplotlib](): `pip install matplotlib`
- [pygraphviz](https://github.com/pygraphviz/pygraphviz): Pygraphviz requires installing the development library of graphviz: (On macOS this can be done via `brew install graphviz`) -- then you can `pip install pygraphviz`
- [adaptagrams](https://github.com/mjwybrow/adaptagrams): First build the adaptagrams c++ libs and then build the swig-based python wrapper.
- [pyhola](https://github.com/shakfu): a pybind11 wrapper of adaptagrams. Follow build instructions in the README and install from the git repo.
- [tsmpy](https://github.com/uknfire/tsmpy): install from git repo
- [OrthogonalDrawing](https://github.com/hasii2011/OrthogonalDrawing): install from git repo
## Caveats
- API Docs are still not available
- The current default layout algorithm is extremely rudimentary, however there are some [promising directions](docs/notes/graph-drawing.md) and you can see also see a [visual comparison](docs/auto-layouts.md) of how well different layout algorithms perform in this context.
- While generation does not consume the py2max objects, Max does not unfortunately refresh-from-file when it's open, so you will have to keep closing and reopening Max to see the changes to the object tree.
- For the few objects which have their own methods, the current implementation differentiates tilde objects from non-tilde objects by providing a different method with a `_tilde` suffix:
```python
gen = p.add_gen()gen_tilde = p.add_gen_tilde()
```## Design Notes
The `.maxpat` JSON format is actually pretty minimal and hierarchical. It has a parent `Patcher` and child `Box` entries and also `Patchlines`. Certain boxes contain other `patcher` instances to represent nested subpatchers and `gen~` patches, etc..
The above structure directly maps onto the Python implementation which consists of 3 classes: `Patcher`, `Box`, and `Patchline`. These classes are extendable via their respective `**kwds` and internal`__dict__` structures. In fact, this is the how the `.from_file` patcher classmethod is implemented.
This turns out to be the most maintainable and flexible way to handle all the differences between the hundreds of Max, MSP, and Jitter objects.
A growing list of patcher methods have been implemented to specialize and facilitate the creation of certain classes of objects which require additional configuration:
- `.add_attr`
- `.add_beap`
- `.add_bpatcher`
- `.add_codebox`
- `.add_coll`
- `.add_comment`
- `.add_dict`
- `.add_floatbox`
- `.add_floatparam`
- `.add_gen`
- `.add_intbox`
- `.add_intparam`
- `.add_itable`
- `.add_message`
- `.add_rnbo`
- `.add_subpatcher`
- `.add_table`
- `.add_textbox`
- `.add_umenu`This is a short list, but the `add_textbox` method alone can handle almost all case. The others are really just there for convenience and to save typing.
Generally, it is recommended to start using `py2max`'s via these `add_` methods, since they have most of the required parameters built into the methods and you can get IDE completion support. Once you are comfortable with the parameters, then use the generic abbreviated form: `add`, which is less typing but tbe tradeoff is you lose the IDE parameter completion support.
## Scripts
The project has a few of scripts which may be useful:
- `convert.py`: convert `maxpat` to `yaml` for ease of reading during dev
- `compare.py`: compare using [deepdiff](https://zepworks.com/deepdiff/current/diff.html)
- `coverage.sh`: run pytest coverage and generate html coverage reportNote that if you want to build py2max as a wheel:
```bash
uv build
```The wheel then should be in the `dist` directory.
## Examples of Use
- [Generate Max patchers for faust2rnbo](https://github.com/grame-cncm/faust/blob/master-dev/architecture/max-msp/rnbo.py)
## Alternative Branches
### pydantic2 branches
There are two experimental branches which use [pydantic2](https://github.com/pydantic/pydantic) as the underlying object backend:
1. [pydantic-2.5.3](https://github.com/shakfu/py2max/tree/pydantic-2.5.3) - based on version `2.5.3` of pydantic2. This was the initial proof-of-concept which achieved the following:
- Tracks the main branch
- 100% tests pass
- More pythonic api
- Improved serialization / deserialization
- Widespread use of type validation based on type-hints.2. [pydantic-2.11.5](https://github.com/shakfu/py2max/tree/pydantic-2.11.5) - This branch uses pydantic2 version `2.11.5`, the latest release available. We created this branch after API changes in newer pydantic2 versions required updates from our initial implementation. Like its predecessor, this branch leverages pydantic2's features while serving as a foundation for developing specialized object classes beyond basic boxes.
```python
>>> from py2max import Patcher
>>> p = Patcher(path='outputs/demo.maxpat')
>>> msg = p.add_message('set')
>>> p.boxes
[Box(id='obj-1', text='set', maxclass='message', numinlets=2, numoutlets=1, outlettype=[''], patching_rect=Rect(x=48.0, y=48.0, w=66.0, h=22.0), patcher=None)]
```Another promising direction of this variant is to create specialized classes for objects which have their own unique `maxclass`. So in this case the above would read:
```python
p.boxes
[Message(id='obj-1', text='set', maxclass='message', numinlets=2, numoutlets=1, outlettype=[''], patching_rect=Rect(x=48.0, y=48.0, w=66.0, h=22.0), patcher=None)]
```### properties branch
There was an early effort to provide property based attribute access and an improved api. It has been supplanted by the `pydantic2` branch and will not be developed further.
## Credits and Licensing
All rights reserved to the original respective authors:
- Steve Kieffer, Tim Dwyer, Kim Marriott, and Michael Wybrow. HOLA: Human-like Orthogonal Network Layout. In Visualization and Computer Graphics, IEEE Transactions on, Volume 22, Issue 1, pages 349 - 358. IEEE, 2016. DOI
- Aric A. Hagberg, Daniel A. Schult and Pieter J. Swart, “Exploring network structure, dynamics, and function using NetworkX”, in Proceedings of the 7th Python in Science Conference (SciPy2008), Gäel Varoquaux, Travis Vaught, and Jarrod Millman (Eds), (Pasadena, CA USA), pp. 11–15, Aug 2008
- A Technique for Drawing Directed Graphs Emden R. Gansner, Eleftherios Koutsofios, Stephen C. North, Kiem-phong Vo • IEEE TRANSACTIONS ON SOFTWARE ENGINEERING • Published 1993
- Gansner, E.R., Koren, Y., North, S. (2005). Graph Drawing by Stress Majorization. In: Pach, J. (eds) Graph Drawing. GD 2004. Lecture Notes in Computer Science, vol 3383. Springer, Berlin, Heidelberg.
- An open graph visualization system and its applications to software engineering Emden R. Gansner, Stephen C. North • SOFTWARE - PRACTICE AND EXPERIENCE • Published 2000