Home

Awesome

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 project.

Features

Possible use cases

Usage examples

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:

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:

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:

p = Patcher.from_file('example1.maxpat')
# ... make some change
p.save_as('example1_mod.maxpat')

Another example with subpatchers:

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_<type> for numbers, numeric parameters, subpatchers, and container-type objects (see the design notes below for more details).

Installation

Simplest way:

git https://github.com/shakfu/py2max.git
cd py2max
pip install . # optional

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:

$ cd py2max
$ ipython

In [1]: from py2max import Patcher

In [2]: p = Patcher.from_file("tests/data/simple.maxpat")

In [3]: p._boxes
Out[3]: [Box(id='obj-2', maxclass='ezdac~'), Box(id='obj-1', maxclass='newobj')]

Testing

py2max has an extensive test suite with tests are in the py2max/tests folder.

One can run all tests as follows:

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:

pytest -v

To check test coverage:

./scripts/coverage.sh

which essentially does the following

mkdir -p outputs
pytest --cov-report html:outputs/_covhtml --cov=py2max tests

To run an individual test:

python3 -m 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):

Caveats

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:

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_<type> 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:

Note that if you want to build py2max as a wheel:

pip install build
cd py2max
python3 -m build .

The wheel then should be in the dist directory.

Examples of Use

Alternative Branches

pydantic2 branch

There is an experimental branch of this project which is based on the pydantic2 project.

This variant has the benefit of the following:


In [1]: from py2max import Patcher

In [2]: p = Patcher(path='outputs/demo.maxpat')

In [3]: msg = p.add_message('set')

In [4]: p.boxes
Out[4]: [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:

In [4]: p.boxes
Out[4]: [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: