py2max 0.3.0__tar.gz → 0.3.1__tar.gz
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-0.3.0 → py2max-0.3.1}/CHANGELOG.md +9 -0
- {py2max-0.3.0 → py2max-0.3.1}/PKG-INFO +60 -2
- {py2max-0.3.0 → py2max-0.3.1}/README.md +59 -1
- {py2max-0.3.0 → py2max-0.3.1}/py2max/__init__.py +1 -1
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/factory.py +109 -3
- {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/legacy.py +7 -0
- {py2max-0.3.0 → py2max-0.3.1}/pyproject.toml +1 -1
- py2max-0.3.1/tests/test_gen.py +96 -0
- py2max-0.3.0/tests/test_gen.py +0 -27
- {py2max-0.3.0 → py2max-0.3.1}/LICENSE +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/__main__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/cli.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/abstract.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/box.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/colors.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/common.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/patcher.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/patchline.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/core/serialization.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/exceptions.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/export/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/export/converters.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/export/svg.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/base.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/flow.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/grid.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/matrix.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/log.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/m4l.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/category.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/data/bundle.json.gz +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/db.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/parser.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/py.typed +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/transformers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/py2max/utils.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/conftest.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/complex.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/desc.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/empty.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/mydevice.amxd +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/mydevice2.amxd +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/nested.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/simple.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/tabular.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/data/umenu.maxref.xml +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/README.md +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/connection_patterns.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/custom_extensions.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/data_containers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/error_handling.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/performance_optimization.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/subpatchers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/api/box_api_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/api/patcher_api_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/auto_layout_demo.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/db/category_db_demo.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/db/maxref_db_demo.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/layout/columnar_layout_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/layout/flow_layout_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/layout/grid_layout_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/layout/matrix_layout_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/basic_synth.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/basic_synth.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/complex_synth.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/complex_synth.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/flow_layout.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/flow_layout.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/grid_layout.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/grid_layout.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/horizontal_layout.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/horizontal_layout.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/styled_no_ports.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/styled_no_title.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/styled_patch.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/styled_with_ports.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/svg_preview_demo.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/vertical_layout.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/vertical_layout.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/workflow_demo.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/workflow_demo.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/quickstart/basic_patch.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/quickstart/layout_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/tutorial/generative_music.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/tutorial/interactive_controller.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/tutorial/signal_processing_chain.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/examples/tutorial/simple_synthesis.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/graphs/random/v30e33.tglf +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/registry.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/scratch.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_abstract_coverage.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_abstraction.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_add.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_amxd.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_attrui.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_basic.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_beap.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_bpatcher.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_cli.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_coll.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_colors.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_colors_theme.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_comment.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_connection_validation.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_converters.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_core_coverage.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_db.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_defaults.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_dict.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_encapsulate.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_error_handling.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_ezdac.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_group.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_itable.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_js.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_kwds_filter.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_builtins.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_coverage.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_flow.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_graph_layout.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_hola1.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_hola2.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_hola3.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_hola_graph.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_matrix.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_networkx1.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_networkx2.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_nx_graphviz.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_nx_orthogonal.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_nx_tsmpy.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_vertical.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_linking.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_m4l.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_maxref.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_maxref_bundle.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_mc_cycle.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_mc_poly.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_message.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_mypatch.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_nested.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_nested_patchers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_number_tilde.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_numbers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_param.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_patcher.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_pitched_osc.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_presets.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_pydantic.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_rnbo.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_rnbo_subpatcher.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_scripting_name.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_search.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_semantic_ids.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_subpatch.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_svg.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_table.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_transformers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_tree.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_tree_builder.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_tutorial_simple_synthesis.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_two_sines.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_umenu.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_utils.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_validate_attrs.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_varname.py +0 -0
- {py2max-0.3.0 → py2max-0.3.1}/tests/test_zl_group.py +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.1]
|
|
6
|
+
|
|
7
|
+
### New: Standalone `gen.codebox~` Support
|
|
8
|
+
|
|
9
|
+
- `Patcher.add_gen_codebox(code)` adds a self-contained `gen.codebox~` object -- a complete gen patch in a single box that lives directly in a regular Max patcher, distinct from the inner `codebox~` (emitted by `add_codebox`) that belongs inside a `gen~`/`rnbo~` subpatcher. This is the form emitted by gen transpilers. Code newlines are normalized to CRLF as Max expects, and `fontname`/`fontsize` default to the monospaced gen style.
|
|
10
|
+
- Inlet/outlet counts are derived automatically from the code (the highest `inN` / `outN` references, floor of 1), matching gen's dynamic-I/O semantics. Explicit `numinlets` / `numoutlets` still override.
|
|
11
|
+
- Available via the `add()` string shortcut too: `p.add("gen.codebox~ out1 = in1 * 0.5;")`. The shortcut suits single-line / `;`-terminated code; pass multi-line source to `add_gen_codebox()` directly.
|
|
12
|
+
- Connection validation for `gen.codebox~` (and `codebox` / `codebox~`) now bound-checks against the box's own declared inlet/outlet counts rather than the static `.maxref.xml` entry, since codebox I/O is code-dependent. This both allows valid connections to/from wider codeboxes (e.g. from a second outlet) and rejects genuinely out-of-range ones.
|
|
13
|
+
|
|
5
14
|
## [0.3.0]
|
|
6
15
|
|
|
7
16
|
### Removed: Interactive Server Split Into `py2max-server` (breaking)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py2max
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A library for offline generation of Max/MSP patcher (.maxpat) files.
|
|
5
5
|
Keywords: Max,maxpat,offline
|
|
6
6
|
Author: Shakeeb Alireza
|
|
@@ -84,8 +84,38 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
|
|
|
84
84
|
|
|
85
85
|
- **Offline Patch Generation** - Create Max patches programmatically without Max running
|
|
86
86
|
- **Round-trip Conversion** - Load, modify, and save existing `.maxpat` files
|
|
87
|
+
- **Max for Live (.amxd)** - Read/write binary `.amxd` device files with presentation-mode helpers
|
|
87
88
|
- **Universal Object Support** - Works with any Max/MSP/Jitter object
|
|
88
|
-
- **
|
|
89
|
+
- **Fully typed** - Passes `mypy --strict`; no runtime dependencies
|
|
90
|
+
- **High Test Coverage** - 420+ tests ensure reliability
|
|
91
|
+
|
|
92
|
+
### Max for Live (.amxd)
|
|
93
|
+
|
|
94
|
+
Generate Max for Live devices directly. `Patcher.save()` / `Patcher.from_file()`
|
|
95
|
+
auto-detect the `.amxd` extension and read/write the binary device format,
|
|
96
|
+
byte-for-byte compatible with Max-exported devices.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from py2max import Patcher
|
|
100
|
+
|
|
101
|
+
# device_type: "audio_effect" (default), "instrument", or "midi_effect"
|
|
102
|
+
p = Patcher('gain.amxd', device_type='audio_effect')
|
|
103
|
+
p.enable_presentation(devicewidth=120) # render Ableton's device strip
|
|
104
|
+
|
|
105
|
+
plugin = p.add_textbox('plugin~') # audio in from Live
|
|
106
|
+
gain = p.add('live.gain~', maxclass='live.gain~')
|
|
107
|
+
plugout = p.add_textbox('plugout~') # audio back to Live
|
|
108
|
+
gain.add_to_presentation([20, 20, 60, 136]) # show the fader in the device
|
|
109
|
+
|
|
110
|
+
p.add_line(plugin, gain, outlet=0, inlet=0)
|
|
111
|
+
p.add_line(gain, plugout, outlet=0, inlet=0)
|
|
112
|
+
p.save() # writes a binary .amxd
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Helpers: `Patcher.enable_presentation(devicewidth=...)`,
|
|
116
|
+
`Box.add_to_presentation([x, y, w, h])` (rejects M4L infrastructure objects and
|
|
117
|
+
rounds fractional coordinates), and `Patcher.enforce_integer_coords()`. M4L
|
|
118
|
+
binary helpers live in `py2max.m4l`.
|
|
89
119
|
|
|
90
120
|
### Interactive Server (separate package)
|
|
91
121
|
|
|
@@ -252,6 +282,34 @@ p.link(sbox, dac)
|
|
|
252
282
|
p.save()
|
|
253
283
|
```
|
|
254
284
|
|
|
285
|
+
### Gen Codebox
|
|
286
|
+
|
|
287
|
+
`add_gen_codebox()` adds a standalone `gen.codebox~` object -- a complete gen
|
|
288
|
+
patch in a single box that sits directly in a regular Max patcher (unlike the
|
|
289
|
+
inner `codebox~` from `add_codebox()`, which belongs inside a `gen~`/`rnbo~`
|
|
290
|
+
subpatcher). Inlet/outlet counts are derived automatically from the highest
|
|
291
|
+
`inN`/`outN` references in the code:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
p = Patcher('fbdelay.maxpat')
|
|
295
|
+
|
|
296
|
+
# 1 inlet (in1), 1 outlet (out1)
|
|
297
|
+
osc = p.add('cycle~ 440')
|
|
298
|
+
cb = p.add_gen_codebox('''
|
|
299
|
+
Param feedback(0.5, min=0.0, max=0.95);
|
|
300
|
+
History fb(0.0);
|
|
301
|
+
out1 = in1 + fb * feedback;
|
|
302
|
+
fb = out1;
|
|
303
|
+
''')
|
|
304
|
+
dac = p.add('ezdac~')
|
|
305
|
+
p.link(osc, cb)
|
|
306
|
+
p.link(cb, dac)
|
|
307
|
+
p.save()
|
|
308
|
+
|
|
309
|
+
# Or via the add() string shortcut (single-line / `;`-terminated code)
|
|
310
|
+
cb = p.add('gen.codebox~ out1 = in1 * 0.5;')
|
|
311
|
+
```
|
|
312
|
+
|
|
255
313
|
### Object Search
|
|
256
314
|
|
|
257
315
|
```python
|
|
@@ -51,8 +51,38 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
|
|
|
51
51
|
|
|
52
52
|
- **Offline Patch Generation** - Create Max patches programmatically without Max running
|
|
53
53
|
- **Round-trip Conversion** - Load, modify, and save existing `.maxpat` files
|
|
54
|
+
- **Max for Live (.amxd)** - Read/write binary `.amxd` device files with presentation-mode helpers
|
|
54
55
|
- **Universal Object Support** - Works with any Max/MSP/Jitter object
|
|
55
|
-
- **
|
|
56
|
+
- **Fully typed** - Passes `mypy --strict`; no runtime dependencies
|
|
57
|
+
- **High Test Coverage** - 420+ tests ensure reliability
|
|
58
|
+
|
|
59
|
+
### Max for Live (.amxd)
|
|
60
|
+
|
|
61
|
+
Generate Max for Live devices directly. `Patcher.save()` / `Patcher.from_file()`
|
|
62
|
+
auto-detect the `.amxd` extension and read/write the binary device format,
|
|
63
|
+
byte-for-byte compatible with Max-exported devices.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from py2max import Patcher
|
|
67
|
+
|
|
68
|
+
# device_type: "audio_effect" (default), "instrument", or "midi_effect"
|
|
69
|
+
p = Patcher('gain.amxd', device_type='audio_effect')
|
|
70
|
+
p.enable_presentation(devicewidth=120) # render Ableton's device strip
|
|
71
|
+
|
|
72
|
+
plugin = p.add_textbox('plugin~') # audio in from Live
|
|
73
|
+
gain = p.add('live.gain~', maxclass='live.gain~')
|
|
74
|
+
plugout = p.add_textbox('plugout~') # audio back to Live
|
|
75
|
+
gain.add_to_presentation([20, 20, 60, 136]) # show the fader in the device
|
|
76
|
+
|
|
77
|
+
p.add_line(plugin, gain, outlet=0, inlet=0)
|
|
78
|
+
p.add_line(gain, plugout, outlet=0, inlet=0)
|
|
79
|
+
p.save() # writes a binary .amxd
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Helpers: `Patcher.enable_presentation(devicewidth=...)`,
|
|
83
|
+
`Box.add_to_presentation([x, y, w, h])` (rejects M4L infrastructure objects and
|
|
84
|
+
rounds fractional coordinates), and `Patcher.enforce_integer_coords()`. M4L
|
|
85
|
+
binary helpers live in `py2max.m4l`.
|
|
56
86
|
|
|
57
87
|
### Interactive Server (separate package)
|
|
58
88
|
|
|
@@ -219,6 +249,34 @@ p.link(sbox, dac)
|
|
|
219
249
|
p.save()
|
|
220
250
|
```
|
|
221
251
|
|
|
252
|
+
### Gen Codebox
|
|
253
|
+
|
|
254
|
+
`add_gen_codebox()` adds a standalone `gen.codebox~` object -- a complete gen
|
|
255
|
+
patch in a single box that sits directly in a regular Max patcher (unlike the
|
|
256
|
+
inner `codebox~` from `add_codebox()`, which belongs inside a `gen~`/`rnbo~`
|
|
257
|
+
subpatcher). Inlet/outlet counts are derived automatically from the highest
|
|
258
|
+
`inN`/`outN` references in the code:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
p = Patcher('fbdelay.maxpat')
|
|
262
|
+
|
|
263
|
+
# 1 inlet (in1), 1 outlet (out1)
|
|
264
|
+
osc = p.add('cycle~ 440')
|
|
265
|
+
cb = p.add_gen_codebox('''
|
|
266
|
+
Param feedback(0.5, min=0.0, max=0.95);
|
|
267
|
+
History fb(0.0);
|
|
268
|
+
out1 = in1 + fb * feedback;
|
|
269
|
+
fb = out1;
|
|
270
|
+
''')
|
|
271
|
+
dac = p.add('ezdac~')
|
|
272
|
+
p.link(osc, cb)
|
|
273
|
+
p.link(cb, dac)
|
|
274
|
+
p.save()
|
|
275
|
+
|
|
276
|
+
# Or via the add() string shortcut (single-line / `;`-terminated code)
|
|
277
|
+
cb = p.add('gen.codebox~ out1 = in1 * 0.5;')
|
|
278
|
+
```
|
|
279
|
+
|
|
222
280
|
### Object Search
|
|
223
281
|
|
|
224
282
|
```python
|
|
@@ -8,6 +8,7 @@ no API change -- adding a new object type means editing this file, not the
|
|
|
8
8
|
core class.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import re
|
|
11
12
|
import warnings
|
|
12
13
|
from typing import (
|
|
13
14
|
TYPE_CHECKING,
|
|
@@ -35,6 +36,23 @@ if TYPE_CHECKING:
|
|
|
35
36
|
|
|
36
37
|
logger = get_logger(__name__)
|
|
37
38
|
|
|
39
|
+
# Max objects whose inlet/outlet counts are determined by their code rather
|
|
40
|
+
# than by a fixed maxref entry. Connection validation for these consults the
|
|
41
|
+
# box's own declared numinlets/numoutlets instead of the static maxref data.
|
|
42
|
+
DYNAMIC_IO_MAXCLASSES = frozenset({"gen.codebox~", "codebox", "codebox~"})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _max_gen_io_index(code: str, kind: str) -> int:
|
|
46
|
+
"""Highest ``in<N>`` / ``out<N>`` index referenced in gen ``code``.
|
|
47
|
+
|
|
48
|
+
gen codeboxes derive their inlet/outlet count from the code: ``in1``,
|
|
49
|
+
``in2``, ... and ``out1``, ``out2``, .... Returns at least 1 since a gen
|
|
50
|
+
codebox always has one signal inlet and one signal outlet.
|
|
51
|
+
"""
|
|
52
|
+
indices = [int(m) for m in re.findall(rf"\b{kind}(\d+)\b", code)]
|
|
53
|
+
return max([1, *indices])
|
|
54
|
+
|
|
55
|
+
|
|
38
56
|
# Box attributes valid on (nearly) every Max object, independent of an object's
|
|
39
57
|
# own maxref attribute set. Used by add_box when validate_attrs is enabled to
|
|
40
58
|
# whitelist universal/jbox attributes plus the structural keys py2max emits, so
|
|
@@ -260,9 +278,41 @@ class BoxFactoryMixin(AbstractPatcher):
|
|
|
260
278
|
src_name = self._get_object_name(src_obj)
|
|
261
279
|
dst_name = self._get_object_name(dst_obj)
|
|
262
280
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
281
|
+
src_dynamic = src_obj.maxclass in DYNAMIC_IO_MAXCLASSES
|
|
282
|
+
dst_dynamic = dst_obj.maxclass in DYNAMIC_IO_MAXCLASSES
|
|
283
|
+
|
|
284
|
+
if src_dynamic or dst_dynamic:
|
|
285
|
+
# Codeboxes derive their inlet/outlet counts from their code,
|
|
286
|
+
# so bound-check indices against the box's own declared counts
|
|
287
|
+
# rather than the fixed maxref entry. Type checking is skipped
|
|
288
|
+
# because codebox I/O is always signal.
|
|
289
|
+
src_outlets = (
|
|
290
|
+
src_obj.numoutlets
|
|
291
|
+
if src_dynamic
|
|
292
|
+
else maxref.get_outlet_count(src_name)
|
|
293
|
+
)
|
|
294
|
+
dst_inlets = (
|
|
295
|
+
dst_obj.numinlets
|
|
296
|
+
if dst_dynamic
|
|
297
|
+
else maxref.get_inlet_count(dst_name)
|
|
298
|
+
)
|
|
299
|
+
error_msg = ""
|
|
300
|
+
if src_outlets is not None and src_outlet >= src_outlets:
|
|
301
|
+
error_msg = (
|
|
302
|
+
f"Object '{src_name}' only has {src_outlets} outlet(s), "
|
|
303
|
+
f"cannot connect from outlet {src_outlet}"
|
|
304
|
+
)
|
|
305
|
+
elif dst_inlets is not None and dst_inlet >= dst_inlets:
|
|
306
|
+
error_msg = (
|
|
307
|
+
f"Object '{dst_name}' only has {dst_inlets} inlet(s), "
|
|
308
|
+
f"cannot connect to inlet {dst_inlet}"
|
|
309
|
+
)
|
|
310
|
+
is_valid = not error_msg
|
|
311
|
+
else:
|
|
312
|
+
is_valid, error_msg = maxref.validate_connection(
|
|
313
|
+
src_name, src_outlet, dst_name, dst_inlet
|
|
314
|
+
)
|
|
315
|
+
|
|
266
316
|
if not is_valid:
|
|
267
317
|
logger.warning(
|
|
268
318
|
f"Connection validation failed: {src_name}[{src_outlet}] -> {dst_name}[{dst_inlet}]: {error_msg}"
|
|
@@ -473,6 +523,11 @@ class BoxFactoryMixin(AbstractPatcher):
|
|
|
473
523
|
return self.add_subpatcher(value, **kwds)
|
|
474
524
|
if maxclass == "gen~":
|
|
475
525
|
return self.add_gen_tilde(**kwds)
|
|
526
|
+
if maxclass == "gen.codebox~":
|
|
527
|
+
# Tail is the gen code. value.split() collapses whitespace, so this
|
|
528
|
+
# shortcut suits single-line/`;`-terminated code; for multi-line
|
|
529
|
+
# source pass it to add_gen_codebox() directly.
|
|
530
|
+
return self.add_gen_codebox(txt, **kwds)
|
|
476
531
|
if maxclass == "rnbo~":
|
|
477
532
|
return self.add_rnbo(value, **kwds)
|
|
478
533
|
return self.add_textbox(text=value, **kwds)
|
|
@@ -542,6 +597,57 @@ class BoxFactoryMixin(AbstractPatcher):
|
|
|
542
597
|
code, patching_rect, id, comment, comment_pos, tilde=True, **kwds
|
|
543
598
|
)
|
|
544
599
|
|
|
600
|
+
def add_gen_codebox(
|
|
601
|
+
self,
|
|
602
|
+
code: str,
|
|
603
|
+
patching_rect: Optional[Rect] = None,
|
|
604
|
+
id: Optional[str] = None,
|
|
605
|
+
numinlets: Optional[int] = None,
|
|
606
|
+
numoutlets: Optional[int] = None,
|
|
607
|
+
outlettype: Optional[List[str]] = None,
|
|
608
|
+
comment: Optional[str] = None,
|
|
609
|
+
comment_pos: Optional[str] = None,
|
|
610
|
+
**kwds: Any,
|
|
611
|
+
) -> "Box":
|
|
612
|
+
"""Add a standalone ``gen.codebox~`` object.
|
|
613
|
+
|
|
614
|
+
Unlike :meth:`add_codebox` (which emits the ``codebox~`` object meant to
|
|
615
|
+
live *inside* a ``gen~`` or ``rnbo~`` subpatcher), this creates the
|
|
616
|
+
self-contained ``gen.codebox~`` object that lives directly in a regular
|
|
617
|
+
Max patcher -- a complete gen patch in a single box, with no subpatcher
|
|
618
|
+
wrapper. This is the form emitted by gen transpilers.
|
|
619
|
+
|
|
620
|
+
A gen codebox's inlet/outlet counts are dynamic: they are determined by
|
|
621
|
+
the highest ``inN`` / ``outN`` references in the code. They are derived
|
|
622
|
+
automatically when ``numinlets`` / ``numoutlets`` are not given (each
|
|
623
|
+
defaults to at least 1).
|
|
624
|
+
"""
|
|
625
|
+
if "\r" not in code:
|
|
626
|
+
code = code.replace("\n", "\r\n")
|
|
627
|
+
|
|
628
|
+
if numinlets is None:
|
|
629
|
+
numinlets = _max_gen_io_index(code, "in")
|
|
630
|
+
if numoutlets is None:
|
|
631
|
+
numoutlets = _max_gen_io_index(code, "out")
|
|
632
|
+
|
|
633
|
+
kwds.setdefault("fontname", "<Monospaced>")
|
|
634
|
+
kwds.setdefault("fontsize", 12.0)
|
|
635
|
+
|
|
636
|
+
return self.add_box(
|
|
637
|
+
Box(
|
|
638
|
+
id=id or self.get_id("gen.codebox~"),
|
|
639
|
+
code=code,
|
|
640
|
+
maxclass="gen.codebox~",
|
|
641
|
+
numinlets=numinlets,
|
|
642
|
+
numoutlets=numoutlets,
|
|
643
|
+
outlettype=outlettype or ["signal"] * numoutlets,
|
|
644
|
+
patching_rect=patching_rect or self.get_pos(),
|
|
645
|
+
**kwds,
|
|
646
|
+
),
|
|
647
|
+
comment,
|
|
648
|
+
comment_pos,
|
|
649
|
+
)
|
|
650
|
+
|
|
545
651
|
def add_message(
|
|
546
652
|
self,
|
|
547
653
|
text: Optional[str] = None,
|
|
@@ -41,6 +41,13 @@ MAXCLASS_DEFAULTS: Dict[str, Dict[str, Any]] = {
|
|
|
41
41
|
"outlettype": [""],
|
|
42
42
|
"patching_rect": Rect(x=191.0, y=118.0, w=200.0, h=200.0),
|
|
43
43
|
},
|
|
44
|
+
"gen.codebox~": {
|
|
45
|
+
"maxclass": "gen.codebox~",
|
|
46
|
+
"numinlets": 1,
|
|
47
|
+
"numoutlets": 1,
|
|
48
|
+
"outlettype": ["signal"],
|
|
49
|
+
"patching_rect": Rect(x=60.0, y=107.0, w=688.0, h=471.0),
|
|
50
|
+
},
|
|
44
51
|
"dial": {
|
|
45
52
|
"maxclass": "dial",
|
|
46
53
|
"numinlets": 1,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from py2max import InvalidConnectionError, Patcher
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_gen():
|
|
7
|
+
p = Patcher("outputs/test_gen.maxpat")
|
|
8
|
+
sbox = p.add_gen("@title windowSync")
|
|
9
|
+
sp = sbox.subpatcher
|
|
10
|
+
i3 = sp.add_textbox("in 3")
|
|
11
|
+
i4 = sp.add_textbox("in 4")
|
|
12
|
+
plus = sp.add_textbox("+")
|
|
13
|
+
sp.add_line(i3, plus)
|
|
14
|
+
sp.add_line(i4, plus)
|
|
15
|
+
p.save()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_gen_tilde():
|
|
19
|
+
p = Patcher("outputs/test_gen_tilde.maxpat")
|
|
20
|
+
sbox = p.add_gen_tilde("@nocache 0") # also p.add_gen(tilde=True)
|
|
21
|
+
sp = sbox.subpatcher
|
|
22
|
+
i1 = sp.add_textbox("in 1")
|
|
23
|
+
i2 = sp.add_textbox("in 2")
|
|
24
|
+
o1 = sp.add_textbox("out 1")
|
|
25
|
+
plus = sp.add_textbox("+")
|
|
26
|
+
sp.add_line(i1, plus)
|
|
27
|
+
sp.add_line(i2, plus, inlet=1)
|
|
28
|
+
sp.add_line(plus, o1)
|
|
29
|
+
p.save()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_gen_codebox():
|
|
33
|
+
code = "Param fb(0.5, min=0.0, max=0.95);\nout1 = in1 * fb;"
|
|
34
|
+
p = Patcher("outputs/test_gen_codebox.maxpat")
|
|
35
|
+
box = p.add_gen_codebox(code)
|
|
36
|
+
d = box.to_dict()["box"]
|
|
37
|
+
|
|
38
|
+
# standalone gen.codebox~ lives directly in a regular ("box") patcher,
|
|
39
|
+
# unlike the inner "codebox~" emitted by add_codebox.
|
|
40
|
+
assert box.maxclass == "gen.codebox~"
|
|
41
|
+
assert p.classnamespace == "box"
|
|
42
|
+
assert box.numinlets == 1
|
|
43
|
+
assert box.numoutlets == 1
|
|
44
|
+
assert d["outlettype"] == ["signal"]
|
|
45
|
+
assert d["fontname"] == "<Monospaced>"
|
|
46
|
+
# newlines are normalized to CRLF as Max expects
|
|
47
|
+
assert "\r\n" in d["code"]
|
|
48
|
+
p.save()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_gen_codebox_multi_io():
|
|
52
|
+
p = Patcher("outputs/test_gen_codebox_multi.maxpat")
|
|
53
|
+
box = p.add_gen_codebox("out1 = in1;\nout2 = in2;", numinlets=2, numoutlets=2)
|
|
54
|
+
assert box.numinlets == 2
|
|
55
|
+
assert box.numoutlets == 2
|
|
56
|
+
d = box.to_dict()["box"]
|
|
57
|
+
assert d["outlettype"] == ["signal", "signal"]
|
|
58
|
+
p.save()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_gen_codebox_io_autoderived_from_code():
|
|
62
|
+
# inlet/outlet counts follow the highest in<N>/out<N> referenced in the code
|
|
63
|
+
p = Patcher("outputs/test_gen_codebox_auto.maxpat")
|
|
64
|
+
box = p.add_gen_codebox("out1 = in1 + in3;\nout2 = in2;")
|
|
65
|
+
assert box.numinlets == 3
|
|
66
|
+
assert box.numoutlets == 2
|
|
67
|
+
# always at least one signal inlet and outlet, even with no references
|
|
68
|
+
bare = p.add_gen_codebox("history h(0.);\nout1 = h;")
|
|
69
|
+
assert bare.numinlets == 1
|
|
70
|
+
assert bare.numoutlets == 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_gen_codebox_string_dispatch():
|
|
74
|
+
p = Patcher("outputs/test_gen_codebox_dispatch.maxpat")
|
|
75
|
+
box = p.add("gen.codebox~ out1 = in1 * 0.5;")
|
|
76
|
+
assert box.maxclass == "gen.codebox~"
|
|
77
|
+
assert box.to_dict()["box"]["code"].startswith("out1 = in1 * 0.5;")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_gen_codebox_connection_validation():
|
|
81
|
+
# validation uses the codebox's declared (dynamic) I/O, not static maxref
|
|
82
|
+
p = Patcher("outputs/test_gen_codebox_validate.maxpat", validate_connections=True)
|
|
83
|
+
box = p.add_gen_codebox("out1 = in1 + in3;\nout2 = in2;") # 3 in, 2 out
|
|
84
|
+
dac = p.add("ezdac~")
|
|
85
|
+
src = p.add("cycle~ 440")
|
|
86
|
+
|
|
87
|
+
# connecting from the second outlet is valid (codebox has 2 outlets)
|
|
88
|
+
p.add_line(box, dac, outlet=1)
|
|
89
|
+
# connecting into the third inlet is valid (codebox has 3 inlets)
|
|
90
|
+
p.add_line(src, box, inlet=2)
|
|
91
|
+
|
|
92
|
+
# out-of-range outlet/inlet are rejected against the box's own counts
|
|
93
|
+
with pytest.raises(InvalidConnectionError):
|
|
94
|
+
p.add_line(box, dac, outlet=5)
|
|
95
|
+
with pytest.raises(InvalidConnectionError):
|
|
96
|
+
p.add_line(src, box, inlet=9)
|
py2max-0.3.0/tests/test_gen.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from py2max import Patcher
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_gen():
|
|
5
|
-
p = Patcher("outputs/test_gen.maxpat")
|
|
6
|
-
sbox = p.add_gen("@title windowSync")
|
|
7
|
-
sp = sbox.subpatcher
|
|
8
|
-
i3 = sp.add_textbox("in 3")
|
|
9
|
-
i4 = sp.add_textbox("in 4")
|
|
10
|
-
plus = sp.add_textbox("+")
|
|
11
|
-
sp.add_line(i3, plus)
|
|
12
|
-
sp.add_line(i4, plus)
|
|
13
|
-
p.save()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_gen_tilde():
|
|
17
|
-
p = Patcher("outputs/test_gen_tilde.maxpat")
|
|
18
|
-
sbox = p.add_gen_tilde("@nocache 0") # also p.add_gen(tilde=True)
|
|
19
|
-
sp = sbox.subpatcher
|
|
20
|
-
i1 = sp.add_textbox("in 1")
|
|
21
|
-
i2 = sp.add_textbox("in 2")
|
|
22
|
-
o1 = sp.add_textbox("out 1")
|
|
23
|
-
plus = sp.add_textbox("+")
|
|
24
|
-
sp.add_line(i1, plus)
|
|
25
|
-
sp.add_line(i2, plus, inlet=1)
|
|
26
|
-
sp.add_line(plus, o1)
|
|
27
|
-
p.save()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|